<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Carlos Eduardo Sotelo Pinto</title>
    <description>The latest articles on DEV Community by Carlos Eduardo Sotelo Pinto (@csotelo).</description>
    <link>https://dev.to/csotelo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F26345%2Fb1fadbe1-1fdf-482f-90ea-f068909eeb24.jpg</url>
      <title>DEV Community: Carlos Eduardo Sotelo Pinto</title>
      <link>https://dev.to/csotelo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/csotelo"/>
    <language>en</language>
    <item>
      <title>VitaeForge: Breaking the ATS Barrier</title>
      <dc:creator>Carlos Eduardo Sotelo Pinto</dc:creator>
      <pubDate>Fri, 29 May 2026 17:00:00 +0000</pubDate>
      <link>https://dev.to/csotelo/vitaeforge-breaking-the-ats-barrier-20pd</link>
      <guid>https://dev.to/csotelo/vitaeforge-breaking-the-ats-barrier-20pd</guid>
      <description>&lt;p&gt;&lt;strong&gt;Open-source ATS Optimization Framework&lt;/strong&gt; | &lt;a href="https://github.com/csotelo/vitaeforge/" rel="noopener noreferrer"&gt;&lt;strong&gt;GitHub Repository&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Abstract
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;VitaeForge&lt;/strong&gt; is an open-source CLI tool that optimizes resumes for Applicant Tracking Systems (ATS). It takes a single career data file (&lt;code&gt;cv.yaml&lt;/code&gt;) and a job description. It returns a tailored PDF with an ATS score, updated keywords, and CAR-formatted experience bullets.&lt;/p&gt;

&lt;p&gt;The tool solves a specific problem: qualified candidates are filtered out by ATS before a human ever reads their resume. The cause is usually keyword mismatch and poor formatting — not a lack of qualifications.&lt;/p&gt;

&lt;p&gt;VitaeForge addresses this through four mechanisms:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Job description analysis&lt;/strong&gt; — extracts required and preferred keywords&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ATS scoring&lt;/strong&gt; — estimates keyword match before and after tailoring&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CAR formatting&lt;/strong&gt; — rewrites experience bullets in Challenge-Action-Result structure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hexagonal architecture&lt;/strong&gt; — supports multiple AI providers via a single interface&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This paper documents the problem, the architecture, the development methodology, and the honest results from personal use.&lt;/p&gt;




&lt;h2&gt;
  
  
  Executive Summary
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;75% of resumes are rejected by ATS before a human reads them&lt;/strong&gt;[^1]. The reason is rarely lack of qualifications. It is keyword mismatch, bad formatting, and bullets that don't communicate impact clearly.&lt;/p&gt;

&lt;p&gt;VitaeForge automates the fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Analyzes&lt;/strong&gt; the job description and extracts required keywords&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scores&lt;/strong&gt; the CV against the JD before and after tailoring&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewrites&lt;/strong&gt; experience bullets in CAR format (Challenge-Action-Result)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generates&lt;/strong&gt; a tailored PDF in under 2 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is open-source (MIT license). One command. One data file. Works with OpenAI, Anthropic, Google, Groq, and local Ollama models.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Introduction: The ATS Barrier
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1.1 The Problem
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;98% of Fortune 500 companies use ATS&lt;/strong&gt;[^3]. These systems filter resumes automatically before a recruiter sees them. Most candidates never know they were rejected by a machine.&lt;/p&gt;

&lt;p&gt;I am a Python Developer with experience in cloud and data engineering. In early 2026, I was applying for jobs and getting no responses. My skills matched the job descriptions. But my resume scored &lt;strong&gt;below 60/100&lt;/strong&gt; on ATS tools like Jobscan.&lt;/p&gt;

&lt;p&gt;The problem was not my qualifications. It was three things:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Keyword mismatch&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ATS matches exact terms, not synonyms&lt;/td&gt;
&lt;td&gt;"Docker Swarm" ≠ "Kubernetes"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bad formatting&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tables and columns break ATS parsers&lt;/td&gt;
&lt;td&gt;Multi-column layout renders as garbled text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Weak bullets&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Generic statements don’t signal impact&lt;/td&gt;
&lt;td&gt;"Led a team" vs. CAR-formatted bullet&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  1.2 Why I Built VitaeForge
&lt;/h3&gt;

&lt;p&gt;I built VitaeForge to solve my own problem. I wanted a tool that would:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read a job description and extract what the ATS is looking for&lt;/li&gt;
&lt;li&gt;Score my CV against those requirements&lt;/li&gt;
&lt;li&gt;Rewrite my experience bullets in CAR format&lt;/li&gt;
&lt;li&gt;Generate a tailored PDF in minutes, not hours&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a developer, I also used this project to practice &lt;strong&gt;Product Owner and Technical PM skills&lt;/strong&gt;. I wrote user stories with Gherkin acceptance criteria, organized work into sprints, and used AI agents in role-specific modes (BA, Architect, QA, Engineer).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"I wasn’t just competing with other candidates — I was competing against algorithms designed to filter me out before a human ever saw my resume."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  2. How ATS Works — and Why Resumes Fail
&lt;/h2&gt;

&lt;h3&gt;
  
  
  2.1 The Numbers
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Statistic&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Resumes rejected by ATS before human review&lt;/td&gt;
&lt;td&gt;75%&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.jobscan.co/blog/ats-statistics/" rel="noopener noreferrer"&gt;Jobscan&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fortune 500 companies using ATS&lt;/td&gt;
&lt;td&gt;98%&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.capterra.com/resources/what-is-an-applicant-tracking-system/" rel="noopener noreferrer"&gt;Capterra&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time a recruiter spends on initial resume review&lt;/td&gt;
&lt;td&gt;7 seconds&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.theladders.com/career-advice/eye-tracking-study-2018" rel="noopener noreferrer"&gt;Ladders&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ATS pass rate improvement with CAR format&lt;/td&gt;
&lt;td&gt;+38%&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.themuse.com/advice/how-to-write-resume-bullet-points" rel="noopener noreferrer"&gt;The Muse&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;3 out of 4 resumes never reach a human. When they do, the reviewer spends 7 seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBBW0NhbmRpZGF0ZSBzdWJtaXRzIHJlc3VtZV0gLS0-IEJbQVRTIHBhcnNlcyBkb2N1bWVudF0KICAgIEIgLS0-IEN7S2V5d29yZCBtYXRjaD99CiAgICBDIC0tPnxOb3wgRFvinYwgQXV0by1yZWplY3RlZF0KICAgIEMgLS0-fFllc3wgRXtGb3JtYXQgcmVhZGFibGU_fQogICAgRSAtLT58Tm98IEQKICAgIEUgLS0-fFllc3wgRntTY29yZSBhYm92ZSB0aHJlc2hvbGQ_fQogICAgRiAtLT58Tm98IEQKICAgIEYgLS0-fFllc3wgR1vinIUgUmVhY2hlcyBodW1hbiByZXZpZXdlcl0KICAgIEcgLS0-IEhbNyBzZWNvbmRzIG9mIGF0dGVudGlvbl0" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBBW0NhbmRpZGF0ZSBzdWJtaXRzIHJlc3VtZV0gLS0-IEJbQVRTIHBhcnNlcyBkb2N1bWVudF0KICAgIEIgLS0-IEN7S2V5d29yZCBtYXRjaD99CiAgICBDIC0tPnxOb3wgRFvinYwgQXV0by1yZWplY3RlZF0KICAgIEMgLS0-fFllc3wgRXtGb3JtYXQgcmVhZGFibGU_fQogICAgRSAtLT58Tm98IEQKICAgIEUgLS0-fFllc3wgRntTY29yZSBhYm92ZSB0aHJlc2hvbGQ_fQogICAgRiAtLT58Tm98IEQKICAgIEYgLS0-fFllc3wgR1vinIUgUmVhY2hlcyBodW1hbiByZXZpZXdlcl0KICAgIEcgLS0-IEhbNyBzZWNvbmRzIG9mIGF0dGVudGlvbl0" alt="ATS rejection flow" width="1904" height="374"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2.2 How ATS Screens Resumes
&lt;/h3&gt;

&lt;p&gt;ATS systems filter candidates in three ways:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Keyword matching&lt;/strong&gt; — exact terms, not synonyms.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JD requires:  "Kubernetes"
Resume says:  "Orchestrated containers using Docker Swarm"
ATS result:   ❌ Skill mismatch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Format parsing&lt;/strong&gt; — complex layouts break the parser.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Resume table:   | Company | Role | Dates |
ATS output:     "Company Role Dates Acme Engineer 2020-2022"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Impact scoring&lt;/strong&gt; — generic bullets don't signal value.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Generic bullet&lt;/th&gt;
&lt;th&gt;CAR-formatted bullet&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"Led a team of engineers."&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Challenge:&lt;/strong&gt; Missed deadlines due to unclear priorities. &lt;strong&gt;Action:&lt;/strong&gt; Implemented Agile sprints. &lt;strong&gt;Result:&lt;/strong&gt; Improved delivery time by 30%.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A JD requiring "Python, Django, AWS" will reject a resume that says "Backend Development, REST APIs, Cloud" — even if the candidate is qualified.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.3 What VitaeForge Changes
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv5eq7icjum5pfdzbllmh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv5eq7icjum5pfdzbllmh.png" alt="VitaeForge Workflow Comparison" width="799" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Without VitaeForge&lt;/th&gt;
&lt;th&gt;With VitaeForge&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ATS Score (observed)&lt;/td&gt;
&lt;td&gt;&amp;lt; 60&lt;/td&gt;
&lt;td&gt;75–90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time per application&lt;/td&gt;
&lt;td&gt;20–40 minutes&lt;/td&gt;
&lt;td&gt;&amp;lt; 2 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keyword alignment&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Automated from JD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bullet format&lt;/td&gt;
&lt;td&gt;Generic&lt;/td&gt;
&lt;td&gt;CAR (Challenge-Action-Result)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  3. Building VitaeForge: A Product Owner’s Journey
&lt;/h2&gt;

&lt;h3&gt;
  
  
  3.1 Methodology
&lt;/h3&gt;

&lt;p&gt;Two disciplines shaped the development process:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ATDD (Acceptance Test-Driven Development)&lt;/strong&gt;&lt;br&gt;
I used the &lt;strong&gt;ATDD skill for Claude Code&lt;/strong&gt; — not the full framework. It gave me the Gherkin/INVEST vocabulary and a role-switching discipline. Each feature started with acceptance criteria. The AI wrote code against those criteria. This reduced ambiguity and kept each session focused.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hexagonal Architecture (Ports &amp;amp; Adapters)&lt;/strong&gt;&lt;br&gt;
Domain logic has zero external dependencies. AI providers, file I/O, and the PDF renderer are all infrastructure. The domain only knows about the &lt;code&gt;AIPort&lt;/code&gt; interface. This made it trivial to swap models and test business logic in isolation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgICBzdWJncmFwaCBEb21haW4KICAgICAgICBVQ1tVc2UgQ2FzZXNcbkFUU1Njb3JlciDCtyBFeHBlcmllbmNlRW5yaWNoZXJcbkpEQW5hbHl6ZXIgwrcgUHJvZmlsZUdlbmVyYXRvcl0KICAgICAgICBNW01vZGVsc1xuQ1ZEYXRhIMK3IEV4cGVyaWVuY2VcbkxvY2FsaXplZFN0cmluZ10KICAgICAgICBQW0FJUG9ydCBpbnRlcmZhY2VdCiAgICBlbmQKICAgIHN1YmdyYXBoIEluZnJhc3RydWN0dXJlCiAgICAgICAgT0FbT3BlbkFJIEFkYXB0ZXJdCiAgICAgICAgQUFbQW50aHJvcGljIEFkYXB0ZXJdCiAgICAgICAgR0FbR29vZ2xlIEFkYXB0ZXJdCiAgICAgICAgT0xbT2xsYW1hIEFkYXB0ZXJdCiAgICAgICAgUltyZW5kZXJjdiBSdW5uZXJdCiAgICAgICAgTFtZQU1MIExvYWRlcnNdCiAgICBlbmQKICAgIFAgLS0-IE9BICYgQUEgJiBHQSAmIE9MCiAgICBVQyAtLT4gUAogICAgVUMgLS0-IE0KICAgIFVDIC0tPiBSCiAgICBVQyAtLT4gTA" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgICBzdWJncmFwaCBEb21haW4KICAgICAgICBVQ1tVc2UgQ2FzZXNcbkFUU1Njb3JlciDCtyBFeHBlcmllbmNlRW5yaWNoZXJcbkpEQW5hbHl6ZXIgwrcgUHJvZmlsZUdlbmVyYXRvcl0KICAgICAgICBNW01vZGVsc1xuQ1ZEYXRhIMK3IEV4cGVyaWVuY2VcbkxvY2FsaXplZFN0cmluZ10KICAgICAgICBQW0FJUG9ydCBpbnRlcmZhY2VdCiAgICBlbmQKICAgIHN1YmdyYXBoIEluZnJhc3RydWN0dXJlCiAgICAgICAgT0FbT3BlbkFJIEFkYXB0ZXJdCiAgICAgICAgQUFbQW50aHJvcGljIEFkYXB0ZXJdCiAgICAgICAgR0FbR29vZ2xlIEFkYXB0ZXJdCiAgICAgICAgT0xbT2xsYW1hIEFkYXB0ZXJdCiAgICAgICAgUltyZW5kZXJjdiBSdW5uZXJdCiAgICAgICAgTFtZQU1MIExvYWRlcnNdCiAgICBlbmQKICAgIFAgLS0-IE9BICYgQUEgJiBHQSAmIE9MCiAgICBVQyAtLT4gUAogICAgVUMgLS0-IE0KICAgIFVDIC0tPiBSCiAgICBVQyAtLT4gTA" alt="Domain architecture" width="1405" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  3.2 Development Workflow
&lt;/h3&gt;

&lt;p&gt;VitaeForge was built by one developer with AI assistants in role-specific modes. This is not an automated pipeline. It is deliberate context switching — each role gets its own session with its own constraints:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;AI Tool&lt;/th&gt;
&lt;th&gt;Primary Focus&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Business Analyst&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Claude Sonnet (claude.ai)&lt;/td&gt;
&lt;td&gt;User stories, Gherkin acceptance criteria&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Software Architect&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Claude Sonnet (Claude Code)&lt;/td&gt;
&lt;td&gt;Hexagonal architecture, domain boundaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;QA Engineer / Tester&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mistral via OpenCode&lt;/td&gt;
&lt;td&gt;Test case generation, edge case review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Software Engineer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Claude Sonnet (Claude Code)&lt;/td&gt;
&lt;td&gt;Implementation, code quality&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each role had its own prompt constraints:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Key constraint&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BA&lt;/td&gt;
&lt;td&gt;Gherkin only. No technical stack language. INVEST compliance.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architect&lt;/td&gt;
&lt;td&gt;Domain-first. No infrastructure imports in domain layer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QA&lt;/td&gt;
&lt;td&gt;Edge cases and failure paths only. No implementation suggestions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engineer&lt;/td&gt;
&lt;td&gt;Architecture-compliant code only. Approved dependencies. No scope additions.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Keeping roles separate prevented the model from drifting into solving adjacent problems. One long session with no role boundaries consistently produced worse results than shorter, focused sessions.&lt;/p&gt;
&lt;h3&gt;
  
  
  3.3 Process
&lt;/h3&gt;

&lt;p&gt;Work was organized in short cycles. Each feature followed this sequence:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fc2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBCQSBhcyBCQSBSb2xlCiAgICBwYXJ0aWNpcGFudCBBcmNoIGFzIEFyY2hpdGVjdCBSb2xlCiAgICBwYXJ0aWNpcGFudCBFbmcgYXMgRW5naW5lZXIgUm9sZQogICAgcGFydGljaXBhbnQgUUEgYXMgUUEgUm9sZQogICAgQkEtPj5BcmNoOiBVc2VyIHN0b3J5ICsgR2hlcmtpbiBjcml0ZXJpYQogICAgQXJjaC0-PkVuZzogQXJjaGl0ZWN0dXJlIGFwcHJvYWNoICsgY29uc3RyYWludHMKICAgIEVuZy0-PlFBOiBJbXBsZW1lbnRhdGlvbgogICAgUUEtPj5Fbmc6IEZhaWx1cmVzIC8gZWRnZSBjYXNlcwogICAgRW5nLT4-UUE6IEZpeGVkIGltcGxlbWVudGF0aW9uCiAgICBRQS0tPj5CQTog4pyFIEFjY2VwdGFuY2UgY3JpdGVyaWEgcGFzcw" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fc2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBCQSBhcyBCQSBSb2xlCiAgICBwYXJ0aWNpcGFudCBBcmNoIGFzIEFyY2hpdGVjdCBSb2xlCiAgICBwYXJ0aWNpcGFudCBFbmcgYXMgRW5naW5lZXIgUm9sZQogICAgcGFydGljaXBhbnQgUUEgYXMgUUEgUm9sZQogICAgQkEtPj5BcmNoOiBVc2VyIHN0b3J5ICsgR2hlcmtpbiBjcml0ZXJpYQogICAgQXJjaC0-PkVuZzogQXJjaGl0ZWN0dXJlIGFwcHJvYWNoICsgY29uc3RyYWludHMKICAgIEVuZy0-PlFBOiBJbXBsZW1lbnRhdGlvbgogICAgUUEtPj5Fbmc6IEZhaWx1cmVzIC8gZWRnZSBjYXNlcwogICAgRW5nLT4-UUE6IEZpeGVkIGltcGxlbWVudGF0aW9uCiAgICBRQS0tPj5CQTog4pyFIEFjY2VwdGFuY2UgY3JpdGVyaWEgcGFzcw" alt="Feature dev sequence" width="1015" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write acceptance criteria in Gherkin&lt;/li&gt;
&lt;li&gt;Get architect review of the approach&lt;/li&gt;
&lt;li&gt;Implement with the engineer role&lt;/li&gt;
&lt;li&gt;Test and review output manually&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Definition of Ready&lt;/strong&gt; (before starting a story):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Acceptance criteria written in Gherkin&lt;/li&gt;
&lt;li&gt;Architecture approach agreed&lt;/li&gt;
&lt;li&gt;Test scenarios defined&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Definition of Done&lt;/strong&gt; (before closing a story):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Acceptance criteria pass&lt;/li&gt;
&lt;li&gt;CAR format verified on output&lt;/li&gt;
&lt;li&gt;PDF renders correctly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example acceptance test:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gherkin"&gt;&lt;code&gt;&lt;span class="kd"&gt;Feature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; Job Description Analysis
  &lt;span class="kn"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; Extract keywords from a Python developer JD
    &lt;span class="err"&gt;Given a job description for "Senior Python Developer" containing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="s"&gt;"""
      Must have: Python, Django, REST APIs.
      Preferred: Kubernetes, AWS, TDD.
      """&lt;/span&gt;
    &lt;span class="nf"&gt;When &lt;/span&gt;VitaeForge analyzes the job description
    &lt;span class="err"&gt;Then it should return required keywords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;["Python",&lt;/span&gt; &lt;span class="err"&gt;"Django",&lt;/span&gt; &lt;span class="err"&gt;"REST&lt;/span&gt; &lt;span class="err"&gt;APIs"]&lt;/span&gt;
    &lt;span class="err"&gt;And preferred keywords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;["Kubernetes",&lt;/span&gt; &lt;span class="err"&gt;"AWS",&lt;/span&gt; &lt;span class="err"&gt;"TDD"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Testing:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What is tested&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unit&lt;/td&gt;
&lt;td&gt;pytest&lt;/td&gt;
&lt;td&gt;Domain models, use case logic, YAML generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integration&lt;/td&gt;
&lt;td&gt;pytest&lt;/td&gt;
&lt;td&gt;cv_writer upsert/append, CLI modes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Human review&lt;/td&gt;
&lt;td&gt;AI-generated content before acceptance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The project ships with &lt;strong&gt;68 tests&lt;/strong&gt; total.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.4 Model Selection
&lt;/h3&gt;

&lt;p&gt;Models were selected based on personal experience during development and practical trade-offs between cost, quality, and availability. The project supports all models through a registry-based adapter pattern — any model can be swapped via the &lt;code&gt;VITAEFORGE_MODEL&lt;/code&gt; environment variable without code changes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Alias&lt;/th&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Used For&lt;/th&gt;
&lt;th&gt;Personal Assessment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-opus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;Development — all roles&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Best overall.&lt;/strong&gt; Handles any task complexity well — BA, architecture, coding. First choice when quality matters.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-sonnet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;Development — coding + BA&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Strong for coding and requirements work.&lt;/strong&gt; Good balance of speed and quality.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gpt-4o-mini&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;Production default — &lt;strong&gt;final choice&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Reliable for structured output.&lt;/strong&gt; Cost-effective, consistent JSON. Tested for VitaeForge CV generation and stayed with this.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-flash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;Production alternative — tested&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Works well for VitaeForge tasks.&lt;/strong&gt; Fast and free-tier friendly. Tested for CV generation; both GPT and Gemini performed well.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;groq-llama&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Groq&lt;/td&gt;
&lt;td&gt;Testing / free-tier&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Acceptable for simple tasks.&lt;/strong&gt; Free and fast, but not suited to complex generation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ollama-mistral&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ollama (local)&lt;/td&gt;
&lt;td&gt;Small scoped tasks&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Only for narrow, simple tasks.&lt;/strong&gt; Struggles with multi-step reasoning or complex prompts.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ollama-llama3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ollama (local)&lt;/td&gt;
&lt;td&gt;Local/offline use&lt;/td&gt;
&lt;td&gt;No API key required.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;em&gt;BigPickle&lt;/em&gt; (OpenCode Zen)&lt;/td&gt;
&lt;td&gt;OpenCode Zen&lt;/td&gt;
&lt;td&gt;ATDD dev role — specific tasks&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Limited use only.&lt;/strong&gt; Worked for very specific, well-scoped tasks when given detailed prompts and descriptions. Used as the developer agent in ATDD. Not suitable for open-ended or complex generation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;em&gt;Nemotron 2&lt;/em&gt; (OpenCode Zen, free)&lt;/td&gt;
&lt;td&gt;NVIDIA via OpenCode Zen&lt;/td&gt;
&lt;td&gt;ATDD dev role — attempted&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not recommended.&lt;/strong&gt; Did not produce usable output for development tasks. Free tier but not worth the tradeoff in quality.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Author's note&lt;/strong&gt;: &lt;code&gt;claude-opus&lt;/code&gt; was the best model across the board for development — BA, architecture, and implementation. &lt;code&gt;claude-sonnet&lt;/code&gt; was the sweet spot for day-to-day coding and specification work. For production use of VitaeForge itself (generating CVs), both &lt;code&gt;gpt-4o-mini&lt;/code&gt; and &lt;code&gt;gemini-flash&lt;/code&gt; worked well; &lt;code&gt;gpt-4o-mini&lt;/code&gt; was the final choice. BigPickle (OpenCode Zen) served for very specific, well-configured dev tasks; Nemotron 2 was not usable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The registry (&lt;code&gt;src/infrastructure/ai/registry.py&lt;/code&gt;) auto-detects the best available model from the environment if &lt;code&gt;VITAEFORGE_MODEL&lt;/code&gt; is not set, using a priority order: &lt;code&gt;gpt-4o-mini&lt;/code&gt; → &lt;code&gt;groq-llama&lt;/code&gt; → &lt;code&gt;gemini-flash&lt;/code&gt; → &lt;code&gt;claude-haiku&lt;/code&gt; → &lt;code&gt;ollama-llama3&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.5 Technical Challenges and Solutions
&lt;/h3&gt;

&lt;p&gt;The key challenges encountered during development are documented in Section 6. A brief summary here:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Challenge&lt;/th&gt;
&lt;th&gt;Root Cause&lt;/th&gt;
&lt;th&gt;Solution Applied&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AI hallucination&lt;/td&gt;
&lt;td&gt;Under-constrained prompts&lt;/td&gt;
&lt;td&gt;Strict CONSTRAINT clauses + human review gates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt scope creep&lt;/td&gt;
&lt;td&gt;Open-ended sessions&lt;/td&gt;
&lt;td&gt;Role-specific prompt contexts with explicit "DO NOT" clauses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ATS formatting fragility&lt;/td&gt;
&lt;td&gt;Complex PDF layouts&lt;/td&gt;
&lt;td&gt;Delegated to rendercv — VitaeForge only controls content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-language consistency&lt;/td&gt;
&lt;td&gt;Free-form translation&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;LocalizedString&lt;/code&gt; value object as core data primitive; lang passed explicitly in all prompts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CAR format compliance&lt;/td&gt;
&lt;td&gt;Unstructured source bullets&lt;/td&gt;
&lt;td&gt;Structured JSON output with separate &lt;code&gt;challenge&lt;/code&gt;/&lt;code&gt;action&lt;/code&gt;/&lt;code&gt;result&lt;/code&gt; fields&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  3.6 Lessons Learned
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Prompt quality beats model quality.&lt;/strong&gt;&lt;br&gt;
A well-constrained prompt on &lt;code&gt;gemini-flash&lt;/code&gt; produced better output than an open prompt on &lt;code&gt;claude-sonnet&lt;/code&gt;. The most effective elements were explicit "DO NOT" clauses, a required output format, and source anchoring. Start there before upgrading the model.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgICBBW0luaXRpYWwgUHJvbXB0XSAtLT4gQntPdXRwdXQgcXVhbGl0eT99CiAgICBCIC0tPnxHb29kfCBDW1VzZSBpbiBwcm9kdWN0aW9uXQogICAgQiAtLT58TWVkaXVtfCBEW0FkZCBjb25zdHJhaW50c10KICAgIEIgLS0-fFBvb3J8IEVbUmV2aXNlIHN0cnVjdHVyZV0KICAgIEQgLS0-IEZbQWRkIG91dHB1dCBmb3JtYXRdCiAgICBFIC0tPiBGCiAgICBGIC0tPiBC" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgICBBW0luaXRpYWwgUHJvbXB0XSAtLT4gQntPdXRwdXQgcXVhbGl0eT99CiAgICBCIC0tPnxHb29kfCBDW1VzZSBpbiBwcm9kdWN0aW9uXQogICAgQiAtLT58TWVkaXVtfCBEW0FkZCBjb25zdHJhaW50c10KICAgIEIgLS0-fFBvb3J8IEVbUmV2aXNlIHN0cnVjdHVyZV0KICAgIEQgLS0-IEZbQWRkIG91dHB1dCBmb3JtYXRdCiAgICBFIC0tPiBGCiAgICBGIC0tPiBC" alt="Prompt refinement loop" width="681" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Choose the right model for the task.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus&lt;/td&gt;
&lt;td&gt;Best overall. Handles BA, architecture, and coding well. Use when quality matters.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet&lt;/td&gt;
&lt;td&gt;Strong for coding and requirements work. Good daily driver.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-4o-mini&lt;/td&gt;
&lt;td&gt;Reliable for structured JSON output. Final choice for VitaeForge production.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini Flash&lt;/td&gt;
&lt;td&gt;Fast and free-tier friendly. Works well for CV generation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mistral (Ollama)&lt;/td&gt;
&lt;td&gt;Only for narrow, simple tasks. Fails on multi-step reasoning.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BigPickle (OpenCode Zen)&lt;/td&gt;
&lt;td&gt;Limited use. Needs very detailed prompts and narrow scope.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nemotron 2 (OpenCode Zen)&lt;/td&gt;
&lt;td&gt;Not recommended. Did not produce usable output.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Start with a capable model. Downgrading to save cost is rational. Starting cheap to later upgrade wastes more time than it saves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Separate roles = less drift.&lt;/strong&gt;&lt;br&gt;
One long session accumulates context and the model starts solving adjacent problems. Short sessions with one role per context produced consistently better results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Hexagonal architecture made model switching free.&lt;/strong&gt;&lt;br&gt;
Changing AI provider required only a &lt;code&gt;.env&lt;/code&gt; change. No code touched. This paid off every time an API limit was hit or a model underperformed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Human review is not optional for resume content.&lt;/strong&gt;&lt;br&gt;
No prompt constraint fully eliminates hallucination. The &lt;code&gt;--jd&lt;/code&gt; confirmation gate and &lt;code&gt;--edit&lt;/code&gt; YAML preview exist so the author reviews every AI output before it is accepted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Delegate rendering, own content.&lt;/strong&gt;&lt;br&gt;
Building a PDF renderer from scratch would have been a detour. Delegating to rendercv kept the focus on the real problem: content generation and ATS alignment.&lt;/p&gt;


&lt;h2&gt;
  
  
  4. Technical Implementation: Hexagonal Architecture and Core Components
&lt;/h2&gt;
&lt;h3&gt;
  
  
  4.1 Hexagonal Architecture Implementation
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuh01q23sdbjqew832e4n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuh01q23sdbjqew832e4n.png" alt="VitaeForge Architecture Diagram" width="799" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;VitaeForge uses hexagonal architecture (Ports &amp;amp; Adapters). Domain logic has no external dependencies. Infrastructure adapters connect to AI providers, files, and the renderer. The three layers are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Domain Layer&lt;/strong&gt; (&lt;code&gt;src/domain/&lt;/code&gt;)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Core Characteristics&lt;/strong&gt;: Complete isolation from external dependencies — no provider SDK imports, no file I/O, no rendering logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key Models&lt;/strong&gt; (Pydantic v2 BaseModel throughout):
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt; &lt;span class="c1"&gt;# src/domain/models.py (actual)
&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LocalizedString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
     &lt;span class="n"&gt;es&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
     &lt;span class="n"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
     &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Lang&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
         &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;es&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Lang&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ES&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;en&lt;/span&gt;

 &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Experience&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
     &lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
     &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LocalizedString&lt;/span&gt;
     &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LocalizedString&lt;/span&gt;
     &lt;span class="n"&gt;start_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
     &lt;span class="n"&gt;end_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
     &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LocalizedString&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
     &lt;span class="n"&gt;aptitudes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
     &lt;span class="n"&gt;is_entrepreneurship&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
     &lt;span class="n"&gt;bullets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LocalizedString&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

 &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CVData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
     &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
     &lt;span class="n"&gt;lastname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
     &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
     &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
     &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LocalizedString&lt;/span&gt;
     &lt;span class="n"&gt;experience&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Experience&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
     &lt;span class="n"&gt;skills&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SkillTag&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
     &lt;span class="n"&gt;education&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Education&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
     &lt;span class="c1"&gt;# ... certifications, courses, projects, languages, achievements
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use Cases&lt;/strong&gt; (&lt;code&gt;src/domain/use_cases/&lt;/code&gt;):

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;jd_analyzer.py&lt;/code&gt;: Extracts role title, seniority, required/preferred keywords, responsibilities from raw JD text&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ats_scorer.py&lt;/code&gt;: Scores CV against JD via AI prompt, returns &lt;code&gt;ATSResult&lt;/code&gt; with score + keyword gaps&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;experience_enricher.py&lt;/code&gt;: Rewrites experience bullets in CAR format, returns enriched &lt;code&gt;CVData&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;profile_generator.py&lt;/code&gt;: Generates role-specific profile summary and per-entry summaries&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cv_editor.py&lt;/code&gt;: Interactive menu-driven editor — brain dump → AI extraction → &lt;code&gt;cv.yaml&lt;/code&gt; upsert&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Application Layer&lt;/strong&gt; (&lt;code&gt;src/application/cv_generator.py&lt;/code&gt;)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Contains a single function &lt;code&gt;generate_rendercv_yaml()&lt;/code&gt; that assembles domain objects into rendercv's YAML schema. It applies theme config (section selection, &lt;code&gt;max_entries&lt;/code&gt;, one-page vs multi-page behavior) and produces a string ready for rendercv to consume.&lt;/li&gt;
&lt;li&gt;No business logic lives here — it is pure assembly and serialization.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Infrastructure Layer&lt;/strong&gt; (&lt;code&gt;src/infrastructure/&lt;/code&gt;)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI adapters&lt;/strong&gt; (&lt;code&gt;src/infrastructure/ai/&lt;/code&gt;): One class per provider, all implementing &lt;code&gt;AIPort.complete(prompt, system) -&amp;gt; str&lt;/code&gt;. Providers: OpenAI, Anthropic, Google, Ollama, plus an OpenAI-compatible base used for Groq, DeepSeek.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistence&lt;/strong&gt; (&lt;code&gt;src/infrastructure/persistence/&lt;/code&gt;): &lt;code&gt;loaders.py&lt;/code&gt; reads &lt;code&gt;cv.yaml&lt;/code&gt;, &lt;code&gt;profile.yaml&lt;/code&gt;, &lt;code&gt;theme.yaml&lt;/code&gt;. &lt;code&gt;cv_writer.py&lt;/code&gt; handles upsert/append write-back for &lt;code&gt;--edit&lt;/code&gt; mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Renderer&lt;/strong&gt; (&lt;code&gt;src/infrastructure/renderer/rendercv_runner.py&lt;/code&gt;): Shells out to the &lt;code&gt;rendercv&lt;/code&gt; CLI to convert the generated YAML to PDF.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The hexagonal architecture enables several critical capabilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Technology independence&lt;/strong&gt;: Core domain logic remains unchanged when upgrading AI models&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testability&lt;/strong&gt;: Each component can be tested in isolation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adaptability&lt;/strong&gt;: New features can be added without modifying existing code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintainability&lt;/strong&gt;: Clear separation of concerns reduces cognitive load&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  4.2 Core Technical Features
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;--jd&lt;/code&gt; mode end-to-end flow:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fc2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBDTEkKICAgIHBhcnRpY2lwYW50IEpEQSBhcyBKREFuYWx5emVyCiAgICBwYXJ0aWNpcGFudCBBVFMgYXMgQVRTU2NvcmVyCiAgICBwYXJ0aWNpcGFudCBFRSBhcyBFeHBlcmllbmNlRW5yaWNoZXIKICAgIHBhcnRpY2lwYW50IEdFTiBhcyBjdl9nZW5lcmF0b3IKICAgIHBhcnRpY2lwYW50IFJDViBhcyByZW5kZXJjdgogICAgQ0xJLT4-SkRBOiByYXcgSkQgdGV4dAogICAgSkRBLS0-PkNMSTogSkRBbmFseXNpcyAoa2V5d29yZHMsIHJvbGUsIHNlbmlvcml0eSkKICAgIENMSS0-PkFUUzogQ1ZEYXRhICsgSkRBbmFseXNpcwogICAgQVRTLS0-PkNMSTogQVRTUmVzdWx0IChzY29yZSwgbWF0Y2hlZCwgbWlzc2luZywgaGVhZGxpbmUpCiAgICBDTEktPj5DTEk6IFNob3cgc2NvcmUg4oaSIHVzZXIgY29uZmlybXMKICAgIENMSS0-PkVFOiBDVkRhdGEgKyBKREFuYWx5c2lzCiAgICBFRS0tPj5DTEk6IGVucmljaGVkIENWRGF0YSAoQ0FSIGJ1bGxldHMpCiAgICBDTEktPj5HRU46IGVucmljaGVkIENWRGF0YSArIHRoZW1lICsgcHJvZmlsZQogICAgR0VOLS0-PkNMSTogcmVuZGVyY3YgWUFNTAogICAgQ0xJLT4-UkNWOiBZQU1MIGZpbGUKICAgIFJDVi0tPj5DTEk6IFBERg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fc2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBDTEkKICAgIHBhcnRpY2lwYW50IEpEQSBhcyBKREFuYWx5emVyCiAgICBwYXJ0aWNpcGFudCBBVFMgYXMgQVRTU2NvcmVyCiAgICBwYXJ0aWNpcGFudCBFRSBhcyBFeHBlcmllbmNlRW5yaWNoZXIKICAgIHBhcnRpY2lwYW50IEdFTiBhcyBjdl9nZW5lcmF0b3IKICAgIHBhcnRpY2lwYW50IFJDViBhcyByZW5kZXJjdgogICAgQ0xJLT4-SkRBOiByYXcgSkQgdGV4dAogICAgSkRBLS0-PkNMSTogSkRBbmFseXNpcyAoa2V5d29yZHMsIHJvbGUsIHNlbmlvcml0eSkKICAgIENMSS0-PkFUUzogQ1ZEYXRhICsgSkRBbmFseXNpcwogICAgQVRTLS0-PkNMSTogQVRTUmVzdWx0IChzY29yZSwgbWF0Y2hlZCwgbWlzc2luZywgaGVhZGxpbmUpCiAgICBDTEktPj5DTEk6IFNob3cgc2NvcmUg4oaSIHVzZXIgY29uZmlybXMKICAgIENMSS0-PkVFOiBDVkRhdGEgKyBKREFuYWx5c2lzCiAgICBFRS0tPj5DTEk6IGVucmljaGVkIENWRGF0YSAoQ0FSIGJ1bGxldHMpCiAgICBDTEktPj5HRU46IGVucmljaGVkIENWRGF0YSArIHRoZW1lICsgcHJvZmlsZQogICAgR0VOLS0-PkNMSTogcmVuZGVyY3YgWUFNTAogICAgQ0xJLT4-UkNWOiBZQU1MIGZpbGUKICAgIFJDVi0tPj5DTEk6IFBERg" alt="JD end-to-end sequence" width="1385" height="697"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  4.2.1 ATS Scoring System (&lt;code&gt;ats_scorer.py&lt;/code&gt;)
&lt;/h4&gt;

&lt;p&gt;The ATS scorer delegates evaluation entirely to the AI model. Rather than implementing local NLP (TF-IDF, cosine similarity), it sends a structured prompt containing CV facts and JD requirements, and receives back a JSON with score, matched keywords, missing keywords, and an optimized headline and summary. This design keeps the scoring logic in a single, swappable AI call rather than a brittle local pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/domain/use_cases/ats_scorer.py (actual implementation)
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ATSScorer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AIPort&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_ai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ai&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CVData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;JDAnalysis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Lang&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ATSResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_PROMPT_TEMPLATE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cv_facts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;_summarize_cv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cv&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;role_title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;jd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role_title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;seniority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;jd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seniority&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;required_keywords&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;required_keywords&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;preferred_keywords&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;preferred_keywords&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;responsibilities&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;- &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;jd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;responsibilities&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_SYSTEM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_parse_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ATSResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;headline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;summary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;ats_keywords&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ats_keywords&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])),&lt;/span&gt;
            &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
            &lt;span class="n"&gt;matched_keywords&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;matched_keywords&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])),&lt;/span&gt;
            &lt;span class="n"&gt;missing_keywords&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing_keywords&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI model returns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;score&lt;/code&gt; — an integer 0–100 estimating ATS compatibility before tailoring&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;matched_keywords&lt;/code&gt; — keywords present in both the CV and the JD&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;missing_keywords&lt;/code&gt; — important JD keywords absent from the CV&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;headline&lt;/code&gt; and &lt;code&gt;summary&lt;/code&gt; — role-tailored replacements for the CV header&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Design rationale&lt;/strong&gt;: Using the language model as the scoring engine means the same model that understands natural language can also judge keyword semantic equivalence (e.g., "serverless" ≈ "AWS Lambda") without hand-crafted synonym maps. The trade-off is that the score is an estimate, not a deterministic calculation — which is honest, since real ATS scoring algorithms are also opaque.&lt;/p&gt;

&lt;h4&gt;
  
  
  4.2.2 CAR Bullet Generation Engine (&lt;code&gt;experience_enricher.py&lt;/code&gt;)
&lt;/h4&gt;

&lt;p&gt;The &lt;code&gt;ExperienceEnricher&lt;/code&gt; use case transforms experience bullets into Challenge-Action-Result format. It sends each bullet to the AI model with a structured prompt that requires JSON output with separate &lt;code&gt;challenge&lt;/code&gt;, &lt;code&gt;action&lt;/code&gt;, and &lt;code&gt;result&lt;/code&gt; fields, then assembles them into a single coherent bullet string. The prompt includes the JD's required keywords so the model prioritizes relevant terminology in the result.&lt;/p&gt;

&lt;p&gt;The CAR structure is:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fawdt0y56e17kuxp36may.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fawdt0y56e17kuxp36may.png" alt="VitaeForge CAR System Framework" width="799" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Challenge&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The business problem or context&lt;/td&gt;
&lt;td&gt;"ETL pipeline had a 40% failure rate due to schema drift"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Action&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Specific action taken&lt;/td&gt;
&lt;td&gt;"Implemented schema validation layer with automated alerting"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Result&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Measurable outcome&lt;/td&gt;
&lt;td&gt;"Reduced pipeline failures by 35% and cut incident response time from 4h to 30min"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key prompt constraint is that the model must use &lt;strong&gt;only information present in the original bullet&lt;/strong&gt;. It cannot invent metrics, extend timelines, or add skills not mentioned in the source. Human confirmation is required before any enriched output is accepted.&lt;/p&gt;

&lt;h4&gt;
  
  
  4.2.3 Theme-Based Formatting (&lt;code&gt;cv_generator.py&lt;/code&gt;)
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;cv_generator.py&lt;/code&gt; in the application layer maps domain objects to rendercv's YAML schema. The output format depends on the active theme's &lt;code&gt;one_page&lt;/code&gt; flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# themes/harmony/theme.yaml (actual)&lt;/span&gt;
&lt;span class="na"&gt;theme_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;harmony&lt;/span&gt;
&lt;span class="na"&gt;one_page&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;sections&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;experience&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Employment History&lt;/span&gt;
    &lt;span class="na"&gt;max_entries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;      &lt;span class="c1"&gt;# AI-ranked by ATS relevance in one_page mode&lt;/span&gt;
    &lt;span class="na"&gt;max_bullets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;      &lt;span class="c1"&gt;# ignored in one_page mode — only summary shown&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;projects&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Projects&lt;/span&gt;
    &lt;span class="na"&gt;optional&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;max_entries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;One-page mode&lt;/strong&gt; (&lt;code&gt;harmony&lt;/code&gt;): experience entries show only the AI-generated summary paragraph. Projects show only &lt;code&gt;name + URL&lt;/code&gt;. The experience pool is ranked by ATS relevance score and capped at &lt;code&gt;max_entries&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-page mode&lt;/strong&gt; (&lt;code&gt;moderncv&lt;/code&gt;, &lt;code&gt;globant&lt;/code&gt;): full bullets, descriptions, tools line, and project details are included.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;cv_generator.py&lt;/code&gt; function &lt;code&gt;generate_rendercv_yaml()&lt;/code&gt; handles all section rendering — there is no separate &lt;code&gt;LayoutEngine&lt;/code&gt; class.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3 Incremental Regeneration
&lt;/h3&gt;

&lt;p&gt;VitaeForge avoids unnecessary AI calls through a hash-based invalidation strategy. Every time &lt;code&gt;vitaeforge --role&lt;/code&gt; runs, it computes a SHA hash of &lt;code&gt;cv.yaml&lt;/code&gt; and stores it in the profile's &lt;code&gt;_meta.cv_hash&lt;/code&gt; field. On subsequent runs, if the hash hasn't changed, the cached profile summary and per-entry &lt;code&gt;profile_summaries&lt;/code&gt; are reused — no AI call is made.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgICBBW3ZpdGFlZm9yZ2UgLS1yb2xlXSAtLT4gQltDb21wdXRlIFNIQSBoYXNoIG9mIGN2LnlhbWxdCiAgICBCIC0tPiBDe0hhc2ggbWF0Y2hlcyBfbWV0YS5jdl9oYXNoP30KICAgIEMgLS0-fFllc3wgRFtVc2UgY2FjaGVkIHN1bW1hcmllc1xuWmVybyBBSSBjYWxsc10KICAgIEMgLS0-fE5vfCBFW0NhbGwgQUkgZm9yIHByb2ZpbGUgc3VtbWFyeV0KICAgIEUgLS0-IEZbQ2FsbCBBSSBmb3IgZWFjaCBleHBlcmllbmNlIGVudHJ5XQogICAgRiAtLT4gR1tXcml0ZSBuZXcgaGFzaCArIHN1bW1hcmllcyB0byBwcm9maWxlLnlhbWxdCiAgICBEIC0tPiBIW2dlbmVyYXRlX3JlbmRlcmN2X3lhbWxdCiAgICBHIC0tPiBICiAgICBIIC0tPiBJW3JlbmRlcmN2IOKGkiBQREZd" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgICBBW3ZpdGFlZm9yZ2UgLS1yb2xlXSAtLT4gQltDb21wdXRlIFNIQSBoYXNoIG9mIGN2LnlhbWxdCiAgICBCIC0tPiBDe0hhc2ggbWF0Y2hlcyBfbWV0YS5jdl9oYXNoP30KICAgIEMgLS0-fFllc3wgRFtVc2UgY2FjaGVkIHN1bW1hcmllc1xuWmVybyBBSSBjYWxsc10KICAgIEMgLS0-fE5vfCBFW0NhbGwgQUkgZm9yIHByb2ZpbGUgc3VtbWFyeV0KICAgIEUgLS0-IEZbQ2FsbCBBSSBmb3IgZWFjaCBleHBlcmllbmNlIGVudHJ5XQogICAgRiAtLT4gR1tXcml0ZSBuZXcgaGFzaCArIHN1bW1hcmllcyB0byBwcm9maWxlLnlhbWxdCiAgICBEIC0tPiBIW2dlbmVyYXRlX3JlbmRlcmN2X3lhbWxdCiAgICBHIC0tPiBICiAgICBIIC0tPiBJW3JlbmRlcmN2IOKGkiBQREZd" alt="Hash cache flow" width="586" height="1142"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# people/carlos_sotelo/profiles/data_engineer__python_aws.yaml&lt;/span&gt;
&lt;span class="na"&gt;_meta&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;cv_hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;abc123def456&lt;/span&gt;       &lt;span class="c1"&gt;# updated automatically on each run&lt;/span&gt;
&lt;span class="na"&gt;profile_summaries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;            &lt;span class="c1"&gt;# per-entry AI summaries, keyed by (company, start_date)&lt;/span&gt;
  &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;company&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Acme Corp&lt;/span&gt;
      &lt;span class="na"&gt;start_date&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2024-01"&lt;/span&gt;
      &lt;span class="na"&gt;ats_score&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;90&lt;/span&gt;
      &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Migrated&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;web&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;app&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;database&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;AWS..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;First run for a role&lt;/strong&gt;: AI generates profile summary + per-entry summaries. Typical wall-clock time depends on the configured model and number of experience entries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subsequent runs (unchanged cv.yaml)&lt;/strong&gt;: Zero AI calls. Output is regenerated from cached YAML in under a second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After editing cv.yaml&lt;/strong&gt;: Only changed or new entries trigger AI regeneration; existing cached summaries are preserved.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;--refresh&lt;/code&gt; flag bypasses the hash check and forces full regeneration when needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.4 AI Adapter Architecture
&lt;/h3&gt;

&lt;p&gt;VitaeForge uses a registry-and-factory pattern to decouple model selection from business logic. Every AI call in the domain layer goes through &lt;code&gt;AIPort&lt;/code&gt; — an abstract interface. The infrastructure layer resolves the concrete adapter at startup from a central registry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/infrastructure/ai/factory.py (actual implementation)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_ai_adapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AIPort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model_alias&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;DEFAULT_MODEL&lt;/span&gt;
    &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;REGISTRY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="c1"&gt;# ModelEntry: provider, model_id, env_key
&lt;/span&gt;    &lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env_key&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="c1"&gt;# Dispatch to the correct adapter class by provider
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anthropic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;AnthropicAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai_compat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;OpenAICompatibleAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;GoogleAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ollama&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;OllamaAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding a new AI provider requires only a new entry in &lt;code&gt;registry.py&lt;/code&gt; and an adapter class — zero changes to domain or application code. This is the Open/Closed Principle applied directly to AI model management.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auto-detection fallback&lt;/strong&gt;: if &lt;code&gt;VITAEFORGE_MODEL&lt;/code&gt; is not set, the factory iterates a priority list and selects the first model whose API key is present in the environment:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgICBBW1N0YXJ0XSAtLT4gQntWSVRBRUZPUkdFX01PREVMIHNldD99CiAgICBCIC0tPnxZZXN8IENbVXNlIGNvbmZpZ3VyZWQgbW9kZWxdCiAgICBCIC0tPnxOb3wgRHtncHQtNG8tbWluaSBrZXkgcHJlc2VudD99CiAgICBEIC0tPnxZZXN8IEVbVXNlIGdwdC00by1taW5pXQogICAgRCAtLT58Tm98IEZ7Z3JvcS1sbGFtYSBrZXkgcHJlc2VudD99CiAgICBGIC0tPnxZZXN8IEdbVXNlIGdyb3EtbGxhbWFdCiAgICBGIC0tPnxOb3wgSHtnZW1pbmktZmxhc2gga2V5IHByZXNlbnQ_fQogICAgSCAtLT58WWVzfCBJW1VzZSBnZW1pbmktZmxhc2hdCiAgICBIIC0tPnxOb3wgSltDb250aW51ZSBkb3duIHByaW9yaXR5IGxpc3QuLi5d" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgICBBW1N0YXJ0XSAtLT4gQntWSVRBRUZPUkdFX01PREVMIHNldD99CiAgICBCIC0tPnxZZXN8IENbVXNlIGNvbmZpZ3VyZWQgbW9kZWxdCiAgICBCIC0tPnxOb3wgRHtncHQtNG8tbWluaSBrZXkgcHJlc2VudD99CiAgICBEIC0tPnxZZXN8IEVbVXNlIGdwdC00by1taW5pXQogICAgRCAtLT58Tm98IEZ7Z3JvcS1sbGFtYSBrZXkgcHJlc2VudD99CiAgICBGIC0tPnxZZXN8IEdbVXNlIGdyb3EtbGxhbWFdCiAgICBGIC0tPnxOb3wgSHtnZW1pbmktZmxhc2gga2V5IHByZXNlbnQ_fQogICAgSCAtLT58WWVzfCBJW1VzZSBnZW1pbmktZmxhc2hdCiAgICBIIC0tPnxOb3wgSltDb250aW51ZSBkb3duIHByaW9yaXR5IGxpc3QuLi5d" alt="Model fallback" width="910" height="1440"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Observed Results
&lt;/h2&gt;

&lt;h3&gt;
  
  
  5.1 What the Tool Produces
&lt;/h3&gt;

&lt;p&gt;This is a v1.0 release. Controlled empirical studies with statistical significance testing are outside the scope of this paper. What follows are honest observations from the author's personal use during the development period.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ATS score behavior&lt;/strong&gt; (observed using Jobscan on personal applications):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Resumes generated without VitaeForge (&lt;code&gt;--role&lt;/code&gt; mode, generic tailoring) typically scored in the 55–70 range against specific JDs when tested on Jobscan.&lt;/li&gt;
&lt;li&gt;Resumes generated with &lt;code&gt;--jd&lt;/code&gt; mode — which extracts required and preferred keywords from the JD and rebuilds the ATS keyword list and headline — consistently scored higher on the same tool, generally in the 75–90 range for roles that matched the candidate's actual background.&lt;/li&gt;
&lt;li&gt;The gap is expected: &lt;code&gt;--jd&lt;/code&gt; mode explicitly targets the JD's terminology, while generic resumes use role-level positioning not tuned to a specific posting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Time per application&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Manual tailoring (editing bullets, rewriting summary, updating keywords for each posting): 20–40 minutes.&lt;/li&gt;
&lt;li&gt;VitaeForge &lt;code&gt;--jd&lt;/code&gt; mode (one command, confirmation prompt, PDF output): under 2 minutes for a cached profile, slightly longer on first run depending on model response time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What the score actually measures&lt;/strong&gt;: VitaeForge's ATS score is the AI model's estimate of keyword overlap and alignment — not a Jobscan score, not access to any real ATS. It is a relative signal, useful for comparing two tailored versions of the same CV, not an absolute guarantee of ATS pass rate.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.2 Personal Context
&lt;/h3&gt;

&lt;p&gt;VitaeForge was built to solve a problem I was actively experiencing: my resume consistently scored below 60 on Jobscan for roles where I met the stated requirements. The gap was keyword terminology — I described my experience in general engineering language while job descriptions used stack-specific vocabulary (e.g., "Apache Airflow" vs "workflow orchestration", "Terraform" vs "infrastructure as code").&lt;/p&gt;

&lt;p&gt;Building the tool changed how I approach applications:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Targeted submissions&lt;/strong&gt;: I now check the ATS score before submitting. If the gap between my profile and the JD is large on missing keywords I genuinely have, &lt;code&gt;--jd&lt;/code&gt; mode closes most of it automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster iteration&lt;/strong&gt;: What used to take 30 minutes per application (manual rewriting) now takes under 2 minutes, leaving time to write a better cover letter or research the company.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single source of truth&lt;/strong&gt;: All career data lives in &lt;code&gt;cv.yaml&lt;/code&gt;. I edit once; every role variant regenerates from the same facts. No more copy-paste divergence between CV versions.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"The problem wasn't my qualifications — it was that my resume spoke human and the ATS was listening for keywords."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is a v1.0 tool. Broader empirical validation — tracking application outcomes across a larger sample, comparing ATS platforms, measuring interview conversion rates — is planned as the project matures and data accumulates.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Technical Challenges and Solutions
&lt;/h2&gt;

&lt;p&gt;This section documents the real challenges encountered during development and the solutions actually implemented in the codebase.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.1 AI Hallucination Control
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Challenge&lt;/strong&gt;: Language models occasionally invented non-existent skills, extended employment dates beyond what &lt;code&gt;cv.yaml&lt;/code&gt; contained, or added achievements with no basis in the source data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Strict constraint prompts with explicit "DO NOT" clauses and mandatory source anchoring. Every prompt that generates experience content includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Actual constraint pattern used throughout use_cases/
&lt;/span&gt;&lt;span class="n"&gt;CONSTRAINTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
CONSTRAINTS:
- Use ONLY information contained in the provided CV data
- Do NOT invent skills, dates, companies, or metrics not present in the source
- Do NOT add achievements not supported by the input
- Return ONLY valid JSON — no explanation, no markdown wrapper
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verification&lt;/strong&gt;: Human review before accepting generated output. The &lt;code&gt;--jd&lt;/code&gt; mode shows the ATS score and asks for confirmation before writing the PDF. The &lt;code&gt;--edit&lt;/code&gt; mode shows a YAML preview before writing to &lt;code&gt;cv.yaml&lt;/code&gt;. Both gates ensure a human sees the AI output before it becomes permanent.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.2 ATS Formatting — Delegated to rendercv
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Challenge&lt;/strong&gt;: Resume formatting is one of the most common ATS failure points. Tables, multi-column layouts, embedded graphics, and non-standard fonts cause parsing errors in many ATS platforms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: VitaeForge does not generate PDF directly. It produces a structured YAML file consumed by &lt;a href="https://github.com/sinaatalay/rendercv" rel="noopener noreferrer"&gt;rendercv&lt;/a&gt;, which handles Typst-based PDF generation with ATS-compatible output. This delegation means VitaeForge inherits rendercv's formatting guarantees without reimplementing them.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;cv_generator.py&lt;/code&gt; application layer maps domain objects to rendercv's YAML schema. The only formatting decision VitaeForge makes is content selection and ordering — not layout rendering.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.3 Domain-Infrastructure Decoupling
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Challenge&lt;/strong&gt;: AI providers change APIs, pricing, and availability. Hardcoding any provider into domain logic would make the system brittle and expensive to maintain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: The &lt;code&gt;AIPort&lt;/code&gt; abstract interface in &lt;code&gt;src/domain/ports/&lt;/code&gt; defines a single method: &lt;code&gt;complete(prompt, system) -&amp;gt; str&lt;/code&gt;. Every use case depends only on this port. The concrete adapter (OpenAI, Anthropic, Google, Ollama) is resolved at startup by the factory and injected — the domain never imports any provider SDK.&lt;/p&gt;

&lt;p&gt;This was validated in practice: switching the default model from &lt;code&gt;gpt-4o-mini&lt;/code&gt; to &lt;code&gt;groq-llama&lt;/code&gt; or &lt;code&gt;gemini-flash&lt;/code&gt; requires only a &lt;code&gt;.env&lt;/code&gt; change, with no code modification.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.4 Multilingual Consistency
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Challenge&lt;/strong&gt;: Generating CV content in both English and Spanish from the same &lt;code&gt;cv.yaml&lt;/code&gt; source requires consistent terminology and register across languages, particularly for technical terms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: The &lt;code&gt;LocalizedString&lt;/code&gt; value object is the core data primitive — every user-visible string in the domain carries both &lt;code&gt;en&lt;/code&gt; and &lt;code&gt;es&lt;/code&gt; variants. AI generation prompts include the target language explicitly. Technical terms (stack names, tool names) are passed through unchanged since they don't translate (e.g., "Python", "Docker", "AWS" remain the same in both languages).&lt;/p&gt;

&lt;p&gt;Human review remains the final quality gate — the author reviews both language outputs before submitting any application.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.5 CAR Format Compliance
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Challenge&lt;/strong&gt;: Getting consistent Challenge-Action-Result format across all generated experience bullets, especially when the source material (&lt;code&gt;cv.yaml&lt;/code&gt; bullets) is written in plain descriptive language.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: The &lt;code&gt;ExperienceEnricher&lt;/code&gt; use case uses a structured prompt that explicitly defines the CAR format and requires JSON output with separate &lt;code&gt;challenge&lt;/code&gt;, &lt;code&gt;action&lt;/code&gt;, and &lt;code&gt;result&lt;/code&gt; fields. The application layer assembles these into a single coherent bullet. This forces the model to decompose the experience before composing the output, which produces more consistent results than asking for a free-form CAR bullet in one shot.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;--edit&lt;/code&gt; mode's "Review bullets" option (option 9) applies this same enrichment interactively, showing original and improved versions side-by-side for human acceptance.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Next Steps
&lt;/h2&gt;

&lt;p&gt;This is v1.0. No formal roadmap exists. Two concrete improvements are planned.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.1 Docker Image
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: Setup requires pyenv, Python 3.12, a virtualenv, &lt;code&gt;pip install&lt;/code&gt;, and a &lt;code&gt;.env&lt;/code&gt; file. That is five steps before running a single command.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Goal&lt;/strong&gt;: One &lt;code&gt;docker run&lt;/code&gt; that mounts &lt;code&gt;cv.yaml&lt;/code&gt;, a JD file, and a &lt;code&gt;.env&lt;/code&gt;, and produces a PDF.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Target workflow&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/people:/app/people &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/jobs:/app/jobs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/generated:/app/generated &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--env-file&lt;/span&gt; .env &lt;span class="se"&gt;\&lt;/span&gt;
  csotelo/vitaeforge &lt;span class="nt"&gt;--jd&lt;/span&gt; /app/jobs/my_jd.txt &lt;span class="nt"&gt;--lang&lt;/span&gt; en
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this enables&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No local Python setup required&lt;/li&gt;
&lt;li&gt;Run on any machine or CI pipeline&lt;/li&gt;
&lt;li&gt;Consistent environment — no version mismatches&lt;/li&gt;
&lt;li&gt;Easier to share with non-technical users&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hexagonal architecture makes this straightforward. There are no database connections, no persistent state, and no background services. The container reads files, calls an AI API, and writes a PDF.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.2 JD Scraping with Playwright
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: &lt;code&gt;--jd&lt;/code&gt; currently accepts a local text file or a URL that returns plain text. Most real job postings are JavaScript-rendered pages (LinkedIn, Indeed, Greenhouse). Copy-pasting the JD manually is the current workaround.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Goal&lt;/strong&gt;: Pass a job posting URL directly. VitaeForge fetches and extracts the description automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Current — manual copy-paste required&lt;/span&gt;
vitaeforge &lt;span class="nt"&gt;--jd&lt;/span&gt; &lt;span class="nb"&gt;jobs&lt;/span&gt;/my_jd.txt &lt;span class="nt"&gt;--lang&lt;/span&gt; en

&lt;span class="c"&gt;# Planned — URL passed directly&lt;/span&gt;
vitaeforge &lt;span class="nt"&gt;--jd&lt;/span&gt; https://linkedin.com/jobs/view/12345 &lt;span class="nt"&gt;--lang&lt;/span&gt; en
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How it fits the architecture&lt;/strong&gt;: The &lt;code&gt;--jd&lt;/code&gt; input is already abstracted in the CLI layer. A Playwright-based scraper would be a new infrastructure adapter — a &lt;code&gt;JDFetcherPort&lt;/code&gt; in the domain, with a &lt;code&gt;PlaywrightAdapter&lt;/code&gt; in infrastructure. The rest of the pipeline (JD analysis, ATS scoring, enrichment, PDF generation) stays unchanged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key constraints for this feature&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Respect &lt;code&gt;robots.txt&lt;/code&gt; and platform terms of service&lt;/li&gt;
&lt;li&gt;Fallback gracefully if a page cannot be scraped (prompt user to paste manually)&lt;/li&gt;
&lt;li&gt;Keep Playwright as an optional dependency — users who don't need scraping should not be required to install a browser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No timeline. No code written yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Conclusion
&lt;/h2&gt;

&lt;h3&gt;
  
  
  8.1 What VitaeForge Does
&lt;/h3&gt;

&lt;p&gt;VitaeForge is a CLI tool. It takes a CV data file and a job description. It returns a tailored PDF with an ATS score and CAR-formatted bullets. It runs in under 2 minutes. It is open-source.&lt;/p&gt;

&lt;p&gt;The core contributions of this v1.0 release:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Contribution&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hexagonal architecture for AI tools&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Domain logic is fully isolated from AI providers. Swap models via &lt;code&gt;.env&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prompt-based ATS scoring&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No local NLP. The AI estimates keyword gaps and generates tailored output in one call.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CAR bullet generation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Structured JSON prompt forces decomposition before composition. Consistent results.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;cv.yaml as single source of truth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One file. Multiple role variants. Bilingual output. Hash-based caching.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ATDD methodology for solo AI development&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Role-specific prompt sessions with explicit constraints. Documented and repeatable.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  8.2 Key Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;ATS filters 75% of resumes before a human reads them. The fix is keyword alignment and CAR formatting — not rewriting your career.&lt;/li&gt;
&lt;li&gt;Prompt engineering matters more than model selection. Constraints beat creativity.&lt;/li&gt;
&lt;li&gt;Hexagonal architecture pays off immediately when you need to swap AI providers.&lt;/li&gt;
&lt;li&gt;Human review is not optional. Always read what the AI generates before it goes on your CV.&lt;/li&gt;
&lt;li&gt;v1.0 is a working tool used in real job searches. It is not a prototype.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  8.3 Try It
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;For job seekers&lt;/strong&gt;: Copy a job description to a text file. Run &lt;code&gt;vitaeforge --jd jobs/my_jd.txt --lang en&lt;/code&gt;. Check the ATS score. Submit the applications that score above 75.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For developers&lt;/strong&gt;: Fork it. Add a provider adapter in 20 lines. Build a new theme. The architecture is designed for extension without modifying existing code.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"If ATS is the gatekeeper, VitaeForge is the key. Try it, fork it, or build on it — just don’t let algorithms decide your career."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;🔗 &lt;a href="https://github.com/csotelo/vitaeforge/" rel="noopener noreferrer"&gt;&lt;strong&gt;GitHub: github.com/csotelo/vitaeforge&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Appendix
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A. ATS Statistics
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Statistic&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;75% of resumes are rejected by ATS&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.jobscan.co/blog/ats-statistics/" rel="noopener noreferrer"&gt;Jobscan&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;98% of Fortune 500 companies use ATS&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.capterra.com/resources/what-is-an-applicant-tracking-system/" rel="noopener noreferrer"&gt;Capterra&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVs with CAR format have 38% higher ATS pass rates&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.themuse.com/advice/how-to-write-resume-bullet-points" rel="noopener noreferrer"&gt;The Muse&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  B. License (MIT)
&lt;/h3&gt;

&lt;p&gt;VitaeForge is licensed under the &lt;strong&gt;MIT License&lt;/strong&gt;. Full text in &lt;a href="https://dev.toLICENSE"&gt;&lt;code&gt;LICENSE&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  C. CLI Quick Reference
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install&lt;/span&gt;
git clone https://github.com/csotelo/vitaeforge
&lt;span class="nb"&gt;cd &lt;/span&gt;vitaeforge
python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env   &lt;span class="c"&gt;# add your API key&lt;/span&gt;

&lt;span class="c"&gt;# Mode 1 — Generic CV by role&lt;/span&gt;
vitaeforge &lt;span class="nt"&gt;--role&lt;/span&gt; data_engineer__python_aws &lt;span class="nt"&gt;--lang&lt;/span&gt; en
vitaeforge &lt;span class="nt"&gt;--role&lt;/span&gt; software_engineer__python_django_fastapi_aws &lt;span class="nt"&gt;--lang&lt;/span&gt; es
vitaeforge &lt;span class="nt"&gt;--role&lt;/span&gt; data_engineer__python_aws &lt;span class="nt"&gt;--lang&lt;/span&gt; en &lt;span class="nt"&gt;--refresh&lt;/span&gt;   &lt;span class="c"&gt;# force AI regen&lt;/span&gt;

&lt;span class="c"&gt;# Mode 2 — Job-specific CV from a JD file&lt;/span&gt;
vitaeforge &lt;span class="nt"&gt;--jd&lt;/span&gt; &lt;span class="nb"&gt;jobs&lt;/span&gt;/my_jd.txt &lt;span class="nt"&gt;--lang&lt;/span&gt; en
vitaeforge &lt;span class="nt"&gt;--jd&lt;/span&gt; &lt;span class="nb"&gt;jobs&lt;/span&gt;/my_jd.txt &lt;span class="nt"&gt;--lang&lt;/span&gt; en &lt;span class="nt"&gt;--model&lt;/span&gt; gemini-flash &lt;span class="nt"&gt;--auto&lt;/span&gt;

&lt;span class="c"&gt;# Interactive CV editor&lt;/span&gt;
vitaeforge &lt;span class="nt"&gt;--edit&lt;/span&gt;
vitaeforge &lt;span class="nt"&gt;--edit&lt;/span&gt; &lt;span class="nt"&gt;--person&lt;/span&gt; jane_doe &lt;span class="nt"&gt;--model&lt;/span&gt; gpt-4o-mini

&lt;span class="c"&gt;# Scaffold a new person&lt;/span&gt;
vitaeforge &lt;span class="nt"&gt;--create-person&lt;/span&gt; jane_doe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;All options:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--role ROLE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Generate generic CV for a stored profile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--jd FILE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Generate job-specific CV from a JD text file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--lang {en,es}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Output language&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--person NAME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Person folder under &lt;code&gt;people/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--model MODEL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AI model alias (overrides &lt;code&gt;VITAEFORGE_MODEL&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--theme THEME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Theme override (&lt;code&gt;harmony&lt;/code&gt;, &lt;code&gt;moderncv&lt;/code&gt;, &lt;code&gt;globant&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--refresh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Force AI profile regeneration (ignores hash cache)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--auto&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Skip confirmation prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--edit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Open interactive CV editor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--create-person NAME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Scaffold a new person directory&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Available model aliases:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Alias&lt;/th&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Free tier&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gpt-4o-mini&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gpt-4o&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-haiku&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-sonnet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-opus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-flash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-pro&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;groq-llama&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Groq&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;groq-mixtral&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Groq&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ollama-llama3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ollama (local)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ollama-mistral&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ollama (local)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h3&gt;
  
  
  D. Tools and Frameworks Used
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Role in VitaeForge&lt;/th&gt;
&lt;th&gt;Link&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;rendercv&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;PDF rendering engine — converts YAML to Typst PDF&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/sinaatalay/rendercv" rel="noopener noreferrer"&gt;github.com/sinaatalay/rendercv&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pydantic v2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Domain model validation and serialization&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.pydantic.dev" rel="noopener noreferrer"&gt;docs.pydantic.dev&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Typer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CLI framework&lt;/td&gt;
&lt;td&gt;&lt;a href="https://typer.tiangolo.com" rel="noopener noreferrer"&gt;typer.tiangolo.com&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PyYAML&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;YAML read/write for &lt;code&gt;cv.yaml&lt;/code&gt; and profiles&lt;/td&gt;
&lt;td&gt;&lt;a href="https://pyyaml.org" rel="noopener noreferrer"&gt;pyyaml.org&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;pytest&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unit and integration testing (68 tests)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://pytest.org" rel="noopener noreferrer"&gt;pytest.org&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Jobscan&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ATS score validation during personal use&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.jobscan.co" rel="noopener noreferrer"&gt;jobscan.co&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Primary development environment&lt;/td&gt;
&lt;td&gt;&lt;a href="https://claude.ai/code" rel="noopener noreferrer"&gt;claude.ai/code&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenCode Zen&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Secondary development tool (BigPickle, Nemotron 2)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://opencode.ai" rel="noopener noreferrer"&gt;opencode.ai&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h3&gt;
  
  
  E. Key References
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reference&lt;/th&gt;
&lt;th&gt;Relevance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cockburn, A. — &lt;em&gt;Hexagonal Architecture&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;Architectural pattern used in VitaeForge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;em&gt;ATDD by Example&lt;/em&gt; — Gärtner, M.&lt;/td&gt;
&lt;td&gt;ATDD methodology and Gherkin specification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Harvard OCS Resume Guide&lt;/td&gt;
&lt;td&gt;Resume terminology and CAR format guidance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The Muse — &lt;em&gt;How to Write Resume Bullet Points&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;CAR format +38% ATS pass rate source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jobscan — &lt;em&gt;ATS Statistics&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;75% rejection rate source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capterra — &lt;em&gt;What is an ATS?&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;98% Fortune 500 usage source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The Ladders — &lt;em&gt;Eye Tracking Study&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;7-second recruiter review source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;csotelo — &lt;em&gt;LangGraph + ATDD Pipeline&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;Prior article: multi-agent ATDD methodology&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>ats</category>
      <category>career</category>
    </item>
    <item>
      <title>Building a Multi-Agent ATDD Pipeline with Hexagonal Architecture</title>
      <dc:creator>Carlos Eduardo Sotelo Pinto</dc:creator>
      <pubDate>Mon, 06 Apr 2026 20:30:48 +0000</pubDate>
      <link>https://dev.to/csotelo/building-a-multi-agent-atdd-pipeline-with-langgraph-and-hexagonal-architecture-5a9k</link>
      <guid>https://dev.to/csotelo/building-a-multi-agent-atdd-pipeline-with-langgraph-and-hexagonal-architecture-5a9k</guid>
      <description>&lt;h1&gt;
  
  
  Building a Multi-Agent ATDD Pipeline with Hexagonal Architecture
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Write the spec, mark the story as ready, walk away. The agents do the rest.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The problem with solo AI development
&lt;/h2&gt;

&lt;p&gt;Building a product solo is brutal.&lt;/p&gt;

&lt;p&gt;You are the PO, the architect, the developer, and the QA — all at the same time. When AI coding agents entered the picture, I didn't see a magic button. I saw a new kind of team member that needed the same thing any team member needs: &lt;strong&gt;clear responsibilities, short tasks, and a verifiable definition of done&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The first thing I tried was the obvious approach: long prompts, one agent, do everything. It failed the way it always fails. The model drifted, lost context, and confidently built the wrong thing.&lt;/p&gt;

&lt;p&gt;Then I applied something I already knew from software architecture:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Divide and conquer.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If a long prompt fails, what about a very short one with a very specific context? What if instead of one agent doing everything, you had &lt;strong&gt;multiple agents — each with a single role, a precise skill, and just enough context to do their job&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;That question led me to build an ATDD orchestrator: a pipeline of specialized AI agents, coordinated by a state machine, that takes a user story from spec to acceptance without human intervention in the technical stages.&lt;/p&gt;

&lt;p&gt;In this article I'll walk through the architecture, the design decisions, and — the main focus — how &lt;strong&gt;hexagonal architecture&lt;/strong&gt; made it possible to support two completely different execution engines behind a single port, without touching the domain or the use cases.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is ATDD and why does it fit AI agents perfectly?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Acceptance Test Driven Development&lt;/strong&gt; says: write acceptance criteria first, then build until they pass. The acceptance criteria — written as Gherkin scenarios — are the only real definition of done.&lt;/p&gt;

&lt;p&gt;This maps naturally to a multi-agent pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Architect&lt;/strong&gt; (human + Claude): define the spec, write the acceptance criteria, refine user stories&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test engineer&lt;/strong&gt; (autonomous): write unit and integration tests in RED — before any implementation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer&lt;/strong&gt; (autonomous): make the RED tests pass — and only that&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tester&lt;/strong&gt; (autonomous): quality gate — regressions, ruff, mypy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ATF worker&lt;/strong&gt; (autonomous): run the Gherkin acceptance scenarios with Playwright + Behave&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each role has a single responsibility. Each transition is triggered by a status change in a file. No agent can skip a stage or self-certify completion.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;story.md → status: inbox
              │
              ▼
        test_engineer  → tests in RED
              │
              ▼
          developer    → tests GREEN
              │
              ▼
            tester     → quality gate
              │
              ▼
          atf_worker   → acceptance scenarios pass
              │
              ▼
        status: done  ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The spec is the contract. The Gherkin scenario is the verdict.&lt;/p&gt;




&lt;h2&gt;
  
  
  The architecture: hexagonal all the way down
&lt;/h2&gt;

&lt;p&gt;The orchestrator follows strict &lt;strong&gt;hexagonal architecture&lt;/strong&gt; (ports and adapters):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;domain          — pure Python, no external dependencies
application     — imports domain only (use cases)
infrastructure  — adapters only: Celery, LangGraph, git, frontmatter, opencode
__main__.py     — entry point, wires everything together
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The domain defines four ports (interfaces):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StoryRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ABC&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;story_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Story&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;story_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_by_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CodeRunner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ABC&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaskQueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ABC&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Inter-worker queue — used internally by the Celery engine.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;story_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PipelineExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ABC&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Starts the full pipeline for a story in INBOX.
    Each engine (Celery, LangGraph) provides its own implementation.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;story_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each use case depends only on these ports. For example, &lt;code&gt;RunDeveloper&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RunDeveloper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;story_repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;story_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;developer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_PROMPT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;story_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;story_id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;story_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;READY_TO_TEST&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_tester&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;story_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;story_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BLOCKED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice: the use case doesn't know &lt;em&gt;what&lt;/em&gt; &lt;code&gt;CodeRunner&lt;/code&gt; is (subprocess? API call? mock?), &lt;em&gt;what&lt;/em&gt; &lt;code&gt;StoryRepository&lt;/code&gt; stores to (files? database?), or &lt;em&gt;how&lt;/em&gt; &lt;code&gt;TaskQueue&lt;/code&gt; delivers the next task (Celery? Redis? LangGraph?).&lt;/p&gt;

&lt;p&gt;This is the key. &lt;strong&gt;The infrastructure is replaceable without touching the domain.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The entry point: &lt;code&gt;__main__.py&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;In hexagonal architecture, the entry point is a &lt;strong&gt;driver&lt;/strong&gt; — it wires the application together and starts the loop. It doesn't belong in &lt;code&gt;infrastructure/&lt;/code&gt;, it belongs at the root of the package.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;python -m atdd_orchestrator&lt;/code&gt; runs &lt;code&gt;__main__.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_make_executor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PipelineExecutor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;PIPELINE_ENGINE&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;langgraph&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;atdd_orchestrator.infrastructure.langgraph.pipeline_executor&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;LangGraphPipelineExecutor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;LangGraphPipelineExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;atdd_orchestrator.infrastructure.celery.pipeline_executor&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;CeleryPipelineExecutor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;CeleryPipelineExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_process_project&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;git_adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;git_adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;repo&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FrontmatterStoryRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;executor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_make_executor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;DispatchStories&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;git_adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has_changes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;git_adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit_and_push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;__main__.py&lt;/code&gt; depends on the &lt;code&gt;PipelineExecutor&lt;/code&gt; port — it never imports Celery or LangGraph directly. The engine is a deployment detail selected by &lt;code&gt;PIPELINE_ENGINE&lt;/code&gt; env var.&lt;/p&gt;

&lt;p&gt;The git operations (pull, commit, push) live in &lt;code&gt;infrastructure/git_adapter.py&lt;/code&gt; — a pure adapter with no business logic. The orchestration loop lives in &lt;code&gt;__main__.py&lt;/code&gt; — where it belongs.&lt;/p&gt;




&lt;h2&gt;
  
  
  The production engine: Celery + Redis
&lt;/h2&gt;

&lt;p&gt;The production implementation uses Celery with Redis as the broker. Each role has a dedicated queue and runs in its own isolated container.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# infrastructure/celery/pipeline_executor.py
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CeleryPipelineExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PipelineExecutor&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;story_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_test_engineer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_project_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;story_id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                      &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inbox&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# infrastructure/celery/tasks.py
&lt;/span&gt;&lt;span class="nd"&gt;@app.task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_developer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ready-to-dev&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;task_developer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;project_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;story_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_deps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;RunDeveloper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;story_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;docker compose up
  ├── redis             ← message broker
  ├── git-sync          ← python -m atdd_orchestrator (polls, dispatches)
  ├── test-engineer     ← celery worker --queues=inbox
  ├── developer         ← celery worker --queues=ready-to-dev
  ├── tester            ← celery worker --queues=ready-to-test
  └── atf-worker        ← celery worker --queues=ready-to-atf
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each role runs in its own isolated container. A slow story blocks only one worker. If a container crashes, Redis preserves the message — the worker retries when it comes back. Multiple stories and projects advance in parallel.&lt;/p&gt;

&lt;p&gt;This is the right engine for production: resilient, isolated, horizontally scalable.&lt;/p&gt;




&lt;h2&gt;
  
  
  The local development engine: LangGraph
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/langchain-ai/langgraph" rel="noopener noreferrer"&gt;LangGraph&lt;/a&gt; is a library for building stateful, graph-based workflows. You define nodes (work units) and edges (transitions), compile the graph, and invoke it with an initial state.&lt;/p&gt;

&lt;p&gt;For local development — one project, no Redis, no Docker — LangGraph is the simpler alternative.&lt;/p&gt;

&lt;h3&gt;
  
  
  State
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PipelineState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TypedDict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;story_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;project_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt;
    &lt;span class="n"&gt;blocked_reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;dev_retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;  &lt;span class="c1"&gt;# prevents infinite loops on repeated failures
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Nodes
&lt;/h3&gt;

&lt;p&gt;Each node calls the existing use case with a &lt;code&gt;GraphRoutedQueue&lt;/code&gt; instead of a real queue adapter. &lt;strong&gt;The graph handles routing — the use cases don't need to know what comes next.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GraphRoutedQueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TaskQueue&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;The use cases call queue.enqueue() as a side effect.
    In LangGraph, that call is intentionally ignored —
    the graph decides the next node by reading the story status.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;story_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After executing the use case, the node reads the current status from the repository (the use case already persisted it) and returns the updated state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;developer_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PipelineState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PipelineState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_deps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project_path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nc"&gt;RunDeveloper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;story_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;  &lt;span class="c1"&gt;# use case already saved BLOCKED to the repo
&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_read_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project_path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;story_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocked_reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Graph with conditional edges
&lt;/h3&gt;

&lt;p&gt;The state machine that is implicit in Celery (queue names, routing keys, scattered enqueue calls) becomes explicit Python code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_route_after_tester&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PipelineState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;READY_TO_ATF&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;atf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;READY_TO_DEV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dev_retries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;MAX_DEV_RETRIES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;developer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# quality gate failed → retry
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;                   &lt;span class="c1"&gt;# blocked or retries exhausted
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_pipeline&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StateGraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PipelineState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test_engineer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_engineer_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;developer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="n"&gt;developer_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tester&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="n"&gt;tester_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;atf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="n"&gt;atf_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_entry_point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test_engineer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_conditional_edges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tester&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_route_after_tester&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;atf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;atf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;developer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;developer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="c1"&gt;# ... other edges
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The retry logic, the blocking conditions, the terminal states — all visible, all in one place.&lt;/p&gt;

&lt;h3&gt;
  
  
  LangGraph executor
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# infrastructure/langgraph/pipeline_executor.py
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LangGraphPipelineExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PipelineExecutor&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;project_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_project_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;project_path&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pipeline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_pipeline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;story_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;story_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;story_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project_path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_project_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INBOX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocked_reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dev_retries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No broker, no worker containers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PIPELINE_ENGINE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;langgraph python &lt;span class="nt"&gt;-m&lt;/span&gt; atdd_orchestrator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The design decisions worth discussing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why is the state stored in files, not a database?
&lt;/h3&gt;

&lt;p&gt;Each user story's state lives in a &lt;code&gt;story.md&lt;/code&gt; file with YAML frontmatter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;US04&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;User can reset their password&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;in-progress:ready-to-test&lt;/span&gt;
&lt;span class="na"&gt;sprint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sprint_02&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping state in the repository means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any agent, any tool, any human can read and update it with a text editor&lt;/li&gt;
&lt;li&gt;The state survives restarts with no migration&lt;/li&gt;
&lt;li&gt;Git history shows every state transition&lt;/li&gt;
&lt;li&gt;The orchestrator doesn't own the state — &lt;strong&gt;the project does&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why is the Architect role never automated?
&lt;/h3&gt;

&lt;p&gt;The architect (human + Claude) defines scope, acceptance criteria, and what the story means. That judgment stays human-controlled.&lt;/p&gt;

&lt;p&gt;Automating that step is how you end up building the wrong thing perfectly. The spec is the contract — someone has to own it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Celery for production and LangGraph for local development?
&lt;/h3&gt;

&lt;p&gt;Celery runs each role in its own isolated container. If the tester container is slow, developer keeps working. If a container crashes, Redis holds the message — the worker picks it up when it restarts. Multiple stories across multiple projects advance simultaneously.&lt;/p&gt;

&lt;p&gt;LangGraph runs the full pipeline in a single process. One slow story blocks the orchestrator for everything else. A mid-pipeline crash leaves the story in an intermediate state with no resume mechanism.&lt;/p&gt;

&lt;p&gt;LangGraph is the right choice when you want to debug a single story locally without standing up the full Docker stack. Celery is the right choice when you're running multiple projects autonomously and you need the system to recover without you.&lt;/p&gt;

&lt;p&gt;Both share the &lt;strong&gt;same domain and the same use cases&lt;/strong&gt;. The engine is only an infrastructure choice.&lt;/p&gt;

&lt;h3&gt;
  
  
  The GraphRoutedQueue pattern
&lt;/h3&gt;

&lt;p&gt;The use cases call &lt;code&gt;self._queue.enqueue("run_tester", story_id)&lt;/code&gt; as a side effect of their execution. In the LangGraph world, that call is meaningless — the graph reads the story status and decides the next node.&lt;/p&gt;

&lt;p&gt;Rather than modifying the use cases (which would break the Celery flow), LangGraph nodes pass a &lt;code&gt;GraphRoutedQueue&lt;/code&gt; that absorbs enqueue calls silently. The use case's core behavior — running the agent, saving the status, raising on failure — is unchanged. Only the routing side effect is suppressed.&lt;/p&gt;

&lt;p&gt;This is dependency injection doing exactly what it's supposed to do.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I tested on a real project
&lt;/h2&gt;

&lt;p&gt;The first project running through this pipeline is &lt;a href="https://github.com/csotelo/atdd-framework/tree/main/atf-ai" rel="noopener noreferrer"&gt;atf-ai&lt;/a&gt; — a CLI tool with Playwright-based acceptance tests. Five user stories, end-to-end:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Story&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;US01 — Scaffolding CLI&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;damaged&lt;/code&gt; (1 failing scenario, known issue)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;US02 — Docker Runner&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;accepted&lt;/code&gt; ✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;US03 — Screenplay Actors &amp;amp; Steps&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;accepted&lt;/code&gt; ✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;US04 — Feedback &amp;amp; State Tracking&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;accepted&lt;/code&gt; ✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;US05 — Reports Pipeline &amp;amp; PyPI&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;accepted&lt;/code&gt; ✓&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four out of five stories went from &lt;code&gt;inbox&lt;/code&gt; to &lt;code&gt;done&lt;/code&gt; autonomously. The fifth is blocked on a known state mismatch that requires architectural review — exactly the kind of thing that should block, not silently pass.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LangGraph checkpointing&lt;/strong&gt; — persist graph state to disk so a local pipeline can resume after a crash without re-running completed stages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt; — emit structured events at each node transition for unified tracing across both engines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-healing architect&lt;/strong&gt; — when a story is &lt;code&gt;blocked&lt;/code&gt;, trigger a Claude session to diagnose and propose a fix&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic worker scaling&lt;/strong&gt; — scale Celery workers per queue based on backlog depth&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Repository
&lt;/h2&gt;

&lt;p&gt;The full source is open:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/csotelo/atdd-framework" rel="noopener noreferrer"&gt;github.com/csotelo/atdd-framework&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;atdd_orchestrator/
├── __main__.py      # entry point — python -m atdd_orchestrator
├── domain/          # Story, Status, ports — pure Python
├── application/     # use cases — depend only on domain
└── infrastructure/
    ├── git_adapter.py            # pure git adapter
    ├── celery/
    │   ├── pipeline_executor.py  # PipelineExecutor → send_task (production)
    │   ├── queue_adapter.py      # TaskQueue → inter-worker enqueue
    │   └── tasks.py              # thin Celery tasks → use cases
    └── langgraph/
        ├── pipeline_executor.py  # PipelineExecutor → graph.invoke (local dev)
        ├── nodes.py              # nodes → use cases (GraphRoutedQueue)
        └── graph.py              # StateGraph with conditional edges + retry logic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;35 tests, 0 failures. No Redis, no OpenCode, no network required to run the test suite — all ports are stubbed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;The thing that surprised me most about this project wasn't the AI part. It was how much the design decisions at the domain level determined what was possible later.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PipelineExecutor&lt;/code&gt; is a four-line abstract class. But because it exists, you can swap the execution engine with an env var, test the entire orchestration logic without Redis or Docker, and reason about Celery and LangGraph as implementation details rather than architectural constraints.&lt;/p&gt;

&lt;p&gt;The entry point lives in &lt;code&gt;__main__.py&lt;/code&gt; — not buried in &lt;code&gt;infrastructure/&lt;/code&gt;. The git adapter lives in &lt;code&gt;infrastructure/git_adapter.py&lt;/code&gt; — a pure adapter with no business logic. Every file is in the layer where it belongs.&lt;/p&gt;

&lt;p&gt;If you're building multi-agent pipelines: get the ports right first. Put things in the right layer. The implementations will follow.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Questions, issues, or contributions: &lt;a href="https://github.com/csotelo/atdd-framework" rel="noopener noreferrer"&gt;github.com/csotelo/atdd-framework&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>testing</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Crafting code crafting mead: Un Viaje de Paciencia y Bugs</title>
      <dc:creator>Carlos Eduardo Sotelo Pinto</dc:creator>
      <pubDate>Fri, 30 Jan 2026 17:32:59 +0000</pubDate>
      <link>https://dev.to/csotelo/crafting-code-crafting-mead-un-viaje-de-paciencia-y-bugs-52if</link>
      <guid>https://dev.to/csotelo/crafting-code-crafting-mead-un-viaje-de-paciencia-y-bugs-52if</guid>
      <description>&lt;p&gt;Soy &lt;strong&gt;Python Developer&lt;/strong&gt; y mi vida profesional ha girado siempre en torno a la ingeniería de datos y el desarrollo de software. Sin embargo, hace cuatro años, mi curiosidad me llevó a un mundo que, a primera vista, parecía opuesto a la lógica binaria: empecé a producir hidromiel. Lo que no sabía entonces es que &lt;strong&gt;hacer código&lt;/strong&gt; y fermentar miel tienen una relación mucho más estrecha de lo que cualquier manual técnico podría explicar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mis Inicios: Del "C" Duro a la Reflexión Activa
&lt;/h2&gt;

&lt;p&gt;Recuerdo claramente cuando empecé a programar. Sentía una mezcla de fascinación y frustración. Podía pasar horas frente al ordenador, viendo cómo cada línea generaba un resultado... o un error. Han pasado más de 30 años desde mi primer programa "real": un simulador de carreras de autos en la universidad, escrito en lenguaje C.&lt;/p&gt;

&lt;p&gt;Era un código muy ortodoxo, "duro y feo" como dirían muchos ahora, pero era mío. &lt;strong&gt;Hacer código&lt;/strong&gt; en ese entonces no implicaba pruebas de integración ni pipelines de CI/CD; era compilar, rezar, corregir el error y volver a compilar. Cuando finalmente funcionó y pude presentarlo en el curso de computación gráfica, la sensación de logro fue indescriptible.&lt;/p&gt;

&lt;p&gt;Con el tiempo, el mercado laboral me dio un golpe de realidad. Yo, que me creía un buen programador en Visual Basic, me topé con "monstruos" del código que me bajaron de mi nube. Aprendí que la vida del desarrollador es un flujo constante de retroalimentación.&lt;/p&gt;

&lt;p&gt;Muchos dirán que sentarse a escribir líneas es aburrido, pero para mí, &lt;strong&gt;hacer código&lt;/strong&gt; es un momento de reflexión activa. Es dejar rienda suelta al cerebro para que fluyan las ideas. Si la música es el lenguaje del alma, el código es el lenguaje de la mente capaz de tangibilizar las ideas: &lt;strong&gt;si puedes imaginarlo, puedes programarlo.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  La Alquimia de la Fermentación: El "Deploy" Orgánico
&lt;/h2&gt;

&lt;p&gt;Cuando empecé a hacer hidromiel, encontré ese mismo "flow". Ver a las levaduras moverse en el mosto era como ver los logs de un servidor en tiempo real. Usé una botella de 20 litros, agua, miel y... sí, levadura de pan (mi versión de "código legacy").&lt;/p&gt;

&lt;p&gt;Conecté una manguera a una botella con agua para liberar el CO2 y ahí estaba ese sonido maravilloso: &lt;em&gt;clock... clock... clock&lt;/em&gt;. Era el ritmo del proceso, similar al cursor parpadeando esperando tu próximo comando.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9xwf91ymwj6d19c39hqo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9xwf91ymwj6d19c39hqo.png" alt="Compilando hidromiel" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pero al igual que en el software, tras el "desarrollo" (fermentación), venía la "refactorización" (maduración). Esperar semanas, maduración en frío... y de pronto, ahí estaba: el néctar de los dioses. Probablemente, si hoy probara esa primera tanda, diría "¿qué diablos es esto?" (igual que cuando reviso mi código de hace 5 años), pero en ese momento fue éxtasis puro.&lt;/p&gt;

&lt;h2&gt;
  
  
  El "Code Review" en la Feria de Hidromieleros
&lt;/h2&gt;

&lt;p&gt;Mi momento de humildad llegó en una feria con otros hidromieleros. Fue como mi primer &lt;em&gt;Code Review&lt;/em&gt; serio.&lt;br&gt;
Llevé mi producto sintiéndome orgulloso, pero al probar las creaciones de mis colegas, me di cuenta de que mi "código" necesitaba optimización:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Adolfo de Cacique:&lt;/strong&gt; Me hablaba de su hidromiel con chispas de roble. Yo, amante de las bebidas secas, solo pude pensar: "Wow, estoy en nada". Su nivel de detalle era como usar una librería avanzada que yo desconocía.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Augusto de Heimdal:&lt;/strong&gt; Me mostró innovaciones de menta y café. Mi cerebro explotó. Fue la inspiración para luego intentar mi propia mezcla (un &lt;em&gt;fork&lt;/em&gt; de su idea, si se quiere).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Skall:&lt;/strong&gt; Sus sabores sutiles eran como un código limpio (&lt;em&gt;Clean Code&lt;/em&gt;), elegante y eficiente, podías beberla una y otra vez.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Ancestro:&lt;/strong&gt; Sus estilos buscando la receta legendaria, compilando las recetas como lo hacian los guerreros vikingos generando codigo legacy.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;La Vikinga:&lt;/strong&gt; Su hidromiel de flores de jamaica tenía un perfil semidulce que empapó mis papilas gustativas, una experiencia de usuario (UX) impecable.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;El Maestro Fernando de Ragnarok:&lt;/strong&gt; Simplemente, una explosión de sabores que no podía comprender. Era como leer el código fuente de un arquitecto principal y no entender cómo logró tal eficiencia.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxco8j766qom60pcttkdp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxco8j766qom60pcttkdp.png" alt="la hora del demo en producción" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Mi conclusión fue clara: no estaba a la altura de estos maestros. Tocaba volver al IDE (o al fermentador), iterar, probar y errar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusión: Crafting Code, Crafting Mead
&lt;/h2&gt;

&lt;p&gt;He pasado por muchas dudas existenciales, desde si debería ser Project Manager o seguir como Python Developer, hasta si debería dejar la hidromiel. Pero disfruto demasiado el proceso. Como dijo &lt;strong&gt;Adolfo Galindo de Cacique&lt;/strong&gt;: &lt;em&gt;"No sabes las maravillas que hace el tiempo en la hidromiel"&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Es cierto. Al igual que al &lt;strong&gt;hacer código&lt;/strong&gt;, en la hidromiel no hay una regla única. Solo es imaginarlo. Si puedes imaginarlo, puedes fermentarlo. Ambos oficios requieren paciencia, conocimiento y una tolerancia alta al fracaso inicial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusión: Paciencia y Pasión
&lt;/h2&gt;

&lt;p&gt;Definitivamente, &lt;strong&gt;hacer código&lt;/strong&gt; o hacer hidromiel requieren paciencia y conocimiento. No hay una regla estricta, solo la imaginación y la disciplina para iterar hasta que el resultado sea elegante, limpio y funcional (o delicioso).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Crafting code, crafting mead.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Un dato curioso de mi vida para cerrar:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mi hijo mayor nació el &lt;strong&gt;13 de septiembre&lt;/strong&gt;, el Día del Programador.&lt;/li&gt;
&lt;li&gt;Mi hija menor nació el &lt;strong&gt;20 de febrero&lt;/strong&gt;, aniversario del lanzamiento de Python.&lt;/li&gt;
&lt;li&gt;Mi marca de hidromiel se llama &lt;strong&gt;"Ojo Negro"&lt;/strong&gt;, en honor a mi abuelito con quien me crié.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Parece que el destino ya tenía el código escrito.&lt;/p&gt;

</description>
      <category>python</category>
      <category>developer</category>
      <category>hidromielero</category>
      <category>meader</category>
    </item>
    <item>
      <title>Django vs. FastAPI o Django + FastAPI 2026: ¿Por qué elegir si puedes tener ambos?</title>
      <dc:creator>Carlos Eduardo Sotelo Pinto</dc:creator>
      <pubDate>Mon, 26 Jan 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/csotelo/django-vs-fastapi-o-django-fastapi-2026-por-que-elegir-si-puedes-tener-ambos-426p</link>
      <guid>https://dev.to/csotelo/django-vs-fastapi-o-django-fastapi-2026-por-que-elegir-si-puedes-tener-ambos-426p</guid>
      <description>&lt;p&gt;La evolución del desarrollo web con Python ha alcanzado un punto de inflexión crítico en 2026. Durante décadas, la comunidad se ha debatido entre la exhaustividad de los marcos de trabajo monolíticos y la agilidad de los micro-frameworks. Sin embargo, la madurez de la computación en la nube, el auge de la inteligencia artificial y la consolidación de las arquitecturas de microservicios han transformado este debate en una búsqueda de sinergias. Django, el gigante de "baterías incluidas", y FastAPI, el líder de la velocidad asíncrona, ya no se perciben como herramientas mutuamente excluyentes, sino como componentes complementarios de una arquitectura moderna de alto nivel.   &lt;/p&gt;

&lt;h2&gt;
  
  
  El estado del arte de Python Web en 2026
&lt;/h2&gt;

&lt;p&gt;En el panorama tecnológico actual, Python se ha consolidado no solo como el lenguaje predilecto para la ciencia de datos, sino como el motor fundamental detrás de las API que alimentan modelos de lenguaje de gran escala (LLM) y sistemas distribuidos masivos. El lanzamiento de Python 3.12, 3.13 y la reciente 3.14 ha traído mejoras significativas en el rendimiento del intérprete y en el manejo de la concurrencia, permitiendo que marcos como Django y FastAPI operen en niveles de eficiencia que anteriormente solo se asociaban con lenguajes compilados como Go o Rust.   &lt;/p&gt;

&lt;p&gt;Django ha respondido a este cambio generacional con su versión 6.0, una actualización que redefine su identidad al integrar capacidades asíncronas profundas y un sistema de tareas nativo que reduce la dependencia de infraestructuras externas complejas.1 Por su parte, FastAPI ha capitalizado la adopción total de Pydantic v2, cuya implementación de núcleo en Rust permite validaciones de datos con una latencia casi nula, convirtiéndolo en el estándar de facto para servicios perimetrales donde cada milisegundo cuenta.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comparativa filosófica y técnica inicial
&lt;/h3&gt;

&lt;p&gt;Para comprender la propuesta de valor de 2026, es necesario desglosar las diferencias fundamentales que rigen a ambos frameworks. Mientras que Django se basa en el principio de "Convención sobre Configuración", buscando la productividad a través de decisiones preestablecidas y seguras, FastAPI se apoya en la "Explicitud y Tipado", permitiendo que el desarrollador componga su stack con libertad total bajo un contrato de datos riguroso.   &lt;/p&gt;

&lt;p&gt;Para comprender la propuesta de valor de 2026, es necesario desglosar las diferencias fundamentales que rigen a ambos frameworks. Mientras que Django se basa en el principio de "Convención sobre Configuración", buscando la productividad a través de decisiones preestablecidas y seguras, FastAPI se apoya en la "Explicitud y Tipado", permitiendo que el desarrollador componga su stack con libertad total bajo un contrato de datos riguroso.   &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterio&lt;/th&gt;
&lt;th&gt;Django 6.0&lt;/th&gt;
&lt;th&gt;FastAPI 2026&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Filosofía Principal&lt;/td&gt;
&lt;td&gt;Baterías incluidas y monolito robusto&lt;/td&gt;
&lt;td&gt;API-first, asíncrono y modular&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rendimiento (RPS)&lt;/td&gt;
&lt;td&gt;Moderado-Alto (Optimizado en 6.0)&lt;/td&gt;
&lt;td&gt;Muy Alto (Líder en Python)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validación de Datos&lt;/td&gt;
&lt;td&gt;Django Forms / Serializers (DRF)&lt;/td&gt;
&lt;td&gt;Pydantic v2 (Basado en Rust)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gestión de Base de Datos&lt;/td&gt;
&lt;td&gt;Django ORM (Síncrono/Asíncrono)&lt;/td&gt;
&lt;td&gt;SQLModel, SQLAlchemy, Tortoise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Documentación&lt;/td&gt;
&lt;td&gt;Manual o vía DRF Spectacular&lt;/td&gt;
&lt;td&gt;Automática (OpenAPI/Swagger/ReDoc)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Curva de Aprendizaje&lt;/td&gt;
&lt;td&gt;Moderada (Ecosistema extenso)&lt;/td&gt;
&lt;td&gt;Baja (Pythonic moderno)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Esta distinción no es solo teórica; impacta directamente en el ciclo de vida de los proyectos. Las empresas que buscan estabilidad a largo plazo y una gestión administrativa interna sin fricciones gravitan hacia Django, mientras que aquellas que desarrollan infraestructuras de microservicios para IA o aplicaciones de tiempo real eligen FastAPI por su capacidad de escalado horizontal y mínima huella de memoria.   &lt;/p&gt;

&lt;h2&gt;
  
  
  El renacimiento de Django 6.0: Más allá del monolito
&lt;/h2&gt;

&lt;p&gt;Django 6.0 representa el mayor salto evolutivo en la historia reciente del framework. Lejos de quedar obsoleto, ha sabido absorber las demandas del mercado para ofrecer una experiencia de desarrollo que equilibra su tradicional robustez con las necesidades de asincronismo moderno. La introducción de un sistema de tareas nativo y mejoras en el stack asíncrono posicionan a Django como el centro de control ideal para aplicaciones complejas.   &lt;/p&gt;

&lt;h3&gt;
  
  
  El nuevo marco de tareas en segundo plano (Django Tasks)
&lt;/h3&gt;

&lt;p&gt;Históricamente, cualquier desarrollador de Django que necesitara enviar un correo electrónico sin bloquear la solicitud del usuario o procesar una imagen pesada debía recurrir a Celery o Redis Queue (RQ). Estas herramientas, aunque potentes, añadían una capa de complejidad operativa significativa. Django 6.0 soluciona esto con su módulo django.tasks, que proporciona una API estandarizada para definir y encolar trabajos fuera del ciclo de solicitud-respuesta.   &lt;/p&gt;

&lt;p&gt;Este marco no es simplemente una envoltura para hilos; es una abstracción completa que permite intercambiar backends según la necesidad. Por defecto, incluye un ImmediateBackend para desarrollo y pruebas, pero se puede configurar fácilmente con un DatabaseBackend para aplicaciones que requieren persistencia de tareas sin infraestructura adicional.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Ejemplo de tarea nativa en Django 6.0
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.tasks&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.core.mail&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;send_mail&lt;/span&gt;

&lt;span class="nd"&gt;@task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queue_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;emails&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;enviar_bienvenida_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email_usuario&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nombre&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;send_mail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bienvenido &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;nombre&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Gracias por unirte a nuestra plataforma.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;from_email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;noreply@empresa.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;recipient_list&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;email_usuario&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La relevancia de esta característica radica en su integración con el sistema de transacciones de la base de datos. Se puede asegurar que una tarea solo se encole si la transacción principal se confirma exitosamente, evitando inconsistencias donde un usuario recibe un correo de bienvenida aunque su registro haya fallado en la base de datos.   &lt;/p&gt;

&lt;h3&gt;
  
  
  Seguridad nativa: Content Security Policy (CSP)
&lt;/h3&gt;

&lt;p&gt;En 2026, la seguridad web es más crítica que nunca debido a la sofisticación de los ataques de inyección de código. Django 6.0 ha dado un paso al frente integrando soporte nativo para la Política de Seguridad de Contenido (CSP). A diferencia de versiones anteriores donde se dependía de middleware externo, ahora el núcleo de Django permite configurar y aplicar estas políticas de manera granular a través de diccionarios en los ajustes del proyecto.   &lt;/p&gt;

&lt;p&gt;Esta protección ayuda a mitigar ataques de Cross-Site Scripting (XSS) y Clickjacking al restringir qué recursos (scripts, estilos, imágenes) puede cargar el navegador y desde qué orígenes. La implementación soporta el uso de "nonces" (números de un solo uso), lo que permite habilitar scripts específicos en línea de forma segura sin debilitar la política global.   &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Componente CSP&lt;/th&gt;
&lt;th&gt;Función en Django 6.0&lt;/th&gt;
&lt;th&gt;Impacto en la Seguridad&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SECURE_CSP&lt;/td&gt;
&lt;td&gt;Define la política estricta aplicada&lt;/td&gt;
&lt;td&gt;Bloquea recursos no autorizados&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SECURE_CSP_REPORT_ONLY&lt;/td&gt;
&lt;td&gt;Modo de prueba para políticas&lt;/td&gt;
&lt;td&gt;Reporta violaciones sin bloquearlas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSP_NONCE&lt;/td&gt;
&lt;td&gt;Context processor para templates&lt;/td&gt;
&lt;td&gt;Permite scripts inline verificados&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Mejoras en el soporte asíncrono y ORM
&lt;/h3&gt;

&lt;p&gt;Aunque Django nació en una era síncrona, su transición a ASGI ha culminado en la versión 6.0 con un soporte asíncrono mucho más robusto. El ORM ahora incluye variantes asíncronas para la mayoría de sus métodos de consulta, prefijados con &lt;code&gt;a&lt;/code&gt; (como &lt;code&gt;aget()&lt;/code&gt;, &lt;code&gt;acreate()&lt;/code&gt;, &lt;code&gt;aupdate()&lt;/code&gt;), permitiendo que las vistas asíncronas interactúen con la base de datos sin bloquear el bucle de eventos. Además, se han introducido herramientas como &lt;code&gt;AsyncPaginator&lt;/code&gt; y &lt;code&gt;AsyncPage&lt;/code&gt; para manejar grandes conjuntos de datos de forma no bloqueante, lo cual es esencial para dashboards de alto tráfico.   &lt;/p&gt;

&lt;h2&gt;
  
  
  FastAPI: La velocidad de la luz en la capa de servicios
&lt;/h2&gt;

&lt;p&gt;Si Django es la fortaleza, FastAPI es el interceptor. En 2026, FastAPI se mantiene como el framework de mayor crecimiento en el ecosistema Python, impulsado por su diseño centrado en el rendimiento y la experiencia del desarrollador (DX). Su capacidad para manejar miles de peticiones simultáneas con una latencia mínima lo hace imbatible en escenarios de microservicios y despliegues serverless.   &lt;/p&gt;

&lt;h3&gt;
  
  
  La revolución de Pydantic v2 y Rust
&lt;/h3&gt;

&lt;p&gt;El corazón de FastAPI en 2026 es Pydantic v2. Al mover la lógica de validación de datos a un núcleo escrito en Rust, Pydantic ha eliminado el "impuesto de Python" en el procesamiento de JSON. Para un arquitecto, esto se traduce en una reducción drástica de los costos de computación y una mejora en la densidad de solicitudes por servidor.   &lt;/p&gt;

&lt;p&gt;Los benchmarks de 2025 y 2026 muestran que FastAPI, gracias a esta integración, compite directamente con Node.js y Go en tareas de I/O bound. La validación de tipos no solo mejora el rendimiento, sino que reduce los errores inducidos por humanos en un estimado del 40%, ya que el sistema detecta inconsistencias antes de que el código llegue a ejecución.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framework / Tecnología&lt;/th&gt;
&lt;th&gt;Requests Per Second (RPS)&lt;/th&gt;
&lt;th&gt;Latencia Media (ms)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FastAPI + Uvicorn&lt;/td&gt;
&lt;td&gt;3,000 - 4,500&lt;/td&gt;
&lt;td&gt;1.2 - 2.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Django + ASGI (Daphne)&lt;/td&gt;
&lt;td&gt;800 - 1,500&lt;/td&gt;
&lt;td&gt;,8.0 - 15.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js (Express)&lt;/td&gt;
&lt;td&gt;2,800 - 4,000&lt;/td&gt;
&lt;td&gt;2.0 - 4.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go (Gin)&lt;/td&gt;
&lt;td&gt;5,000 - 7,000&lt;/td&gt;
&lt;td&gt;0.5 - 1.5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Nota:&lt;/strong&gt; Los valores son aproximados basados en benchmarks de 2025 para cargas de trabajo estándar de API. &lt;/p&gt;

&lt;h3&gt;
  
  
  Documentación automática y estándares abiertos
&lt;/h3&gt;

&lt;p&gt;Uno de los mayores atractivos de FastAPI es que la documentación no es una tarea secundaria, sino un subproducto natural del desarrollo. Al utilizar tipos de Python estándar, FastAPI genera esquemas OpenAPI y JSON Schema en tiempo real. En 2026, las interfaces de Swagger UI y ReDoc integradas han evolucionado para permitir pruebas interactivas complejas, incluyendo flujos de autenticación OAuth2 y WebSockets, lo que reduce el tiempo de integración entre equipos de frontend y backend en un 40%.   &lt;/p&gt;

&lt;h3&gt;
  
  
  Seguridad granular con OAuth2 Scopes
&lt;/h3&gt;

&lt;p&gt;A diferencia de Django, que ofrece un sistema de autenticación centralizado y a veces rígido, FastAPI proporciona herramientas para construir sistemas de seguridad altamente granulares utilizando alcances (scopes) de OAuth2. Esto permite definir permisos específicos para cada punto final (endpoint), facilitando arquitecturas donde diferentes aplicaciones o usuarios tienen niveles de acceso drásticamente distintos sobre la misma API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Ejemplo de seguridad por scopes en FastAPI
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Security&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi.security&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OAuth2PasswordBearer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SecurityScopes&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_current_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;security_scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SecurityScopes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OAuth2PasswordBearer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokenUrl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))):&lt;/span&gt;
    &lt;span class="c1"&gt;# Lógica para validar JWT y verificar que posee los scopes requeridos
&lt;/span&gt;    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/items/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dependencies&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este enfoque es ideal para plataformas B2B donde la seguridad debe ser auditada y restringida al nivel de acción individual.   &lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Por qué elegir si puedes tener ambos? La arquitectura híbrida
&lt;/h2&gt;

&lt;p&gt;La tesis central es que la competencia entre Django y FastAPI es un falso dilema. Los sistemas más robustos y escalables de la actualidad utilizan una combinación de ambos, aprovechando sus fortalezas específicas para diferentes capas de la aplicación.   &lt;/p&gt;

&lt;h3&gt;
  
  
  Django como el "Core" de gestión y verdad
&lt;/h3&gt;

&lt;p&gt;En una arquitectura de microservicios moderna, Django asume el rol de "Centro de Comando". Sus responsabilidades incluyen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Gestión de Datos Primaria:&lt;/strong&gt; Utilizar el ORM de Django para definir la estructura de la base de datos y manejar migraciones complejas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Panel Administrativo:&lt;/strong&gt; Proporcionar una interfaz de usuario inmediata para que los operadores de negocio gestionen usuarios, catálogos y configuraciones.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autenticación Central:&lt;/strong&gt; Actuar como el servicio de identidad que emite tokens JWT para el resto del ecosistema.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backoffice:&lt;/strong&gt; Manejar flujos de trabajo internos que no requieren la latencia de microsegundos de FastAPI pero sí una alta fiabilidad y seguridad integrada.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  FastAPI para servicios perimetrales y alto rendimiento
&lt;/h3&gt;

&lt;p&gt;FastAPI se despliega en la periferia, encargándose de las tareas intensivas en I/O y aquellas que requieren escalado masivo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Public API:&lt;/strong&gt; Servir datos a las aplicaciones móviles y web con la menor latencia posible.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inferencia de IA:&lt;/strong&gt; Exponer modelos de aprendizaje automático, aprovechando su asincronismo para no bloquear el servidor mientras se espera la respuesta del modelo.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebSockets y Tiempo Real:&lt;/strong&gt; Manejar conexiones persistentes para chats, notificaciones o telemetría IoT.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Microservicios de Procesamiento:&lt;/strong&gt; Servicios ligeros que realizan transformaciones de datos rápidas o actúan como proxies inteligentes.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Ventajas de la orquestación con Docker
&lt;/h3&gt;

&lt;p&gt;Al desacoplar estas responsabilidades mediante contenedores Docker, se obtienen beneficios operativos críticos para cualquier arquitectura empresarial en 2026 :   &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Disponibilidad Aumentada:&lt;/strong&gt; Una falla crítica en el servicio de FastAPI (por ejemplo, por un pico de tráfico en la API pública) no afecta la capacidad de los administradores para entrar al panel de Django y gestionar la crisis.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mantenimiento sin Downtime:&lt;/strong&gt; Es posible actualizar el núcleo de Django sin detener los servicios de FastAPI, o viceversa. Esto permite una integración y despliegue continuos (CI/CD) mucho más ágiles.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Escalabilidad Granular:&lt;/strong&gt; Si la API pública experimenta mucha demanda, se pueden lanzar 10 instancias adicionales del contenedor de FastAPI sin tener que replicar todo el pesado monolito de Django, optimizando el uso de recursos en la nube.
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Implementación práctica: Django Models dentro de FastAPI
&lt;/h2&gt;

&lt;p&gt;Uno de los mayores retos de la arquitectura híbrida es evitar la duplicación de la lógica de negocio y los modelos de datos. En 2026, la técnica recomendada es inicializar el contexto de Django dentro de la aplicación FastAPI para consumir el ORM directamente cuando sea necesario.   &lt;/p&gt;

&lt;h3&gt;
  
  
  Estructura del proyecto híbrido
&lt;/h3&gt;

&lt;p&gt;Para que esta integración funcione, el proyecto debe seguir una estructura de directorios que permita la coexistencia de ambos frameworks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/proyecto_soberano/ 
├── core_django/ &lt;span class="c"&gt;# Proyecto Django (Admin, Migrations, Models) &lt;/span&gt;
│ ├── settings.py 
│ └── models.py 
├── api_fastapi/ &lt;span class="c"&gt;# Microservicio FastAPI &lt;/span&gt;
│ ├── main.py 
│ └── dependencies.py 
├── docker-compose.yml 
└── shared_requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  El proceso de "Bootstrapping"
&lt;/h3&gt;

&lt;p&gt;Para usar los modelos de Django en FastAPI, es imperativo configurar el entorno antes de realizar cualquier importación. Esto se hace en el punto de entrada de la aplicación FastAPI :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# api_fastapi/main.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;django&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;

&lt;span class="c1"&gt;# 1. Configurar el módulo de ajustes de Django
&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DJANGO_SETTINGS_MODULE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;core_django.settings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 2. Inicializar Django
&lt;/span&gt;&lt;span class="n"&gt;django&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# 3. Ahora es seguro importar modelos
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;core_django.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Producto&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Servicio de Consulta Rápida&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/producto/{id}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;obtener_producto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Uso del ORM de Django dentro de FastAPI
&lt;/span&gt;    &lt;span class="c1"&gt;# Nota: Django ORM es síncrono por defecto, FastAPI maneja esto en hilos
&lt;/span&gt;    &lt;span class="n"&gt;producto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Producto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nombre&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;precio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;producto&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este enfoque permite que FastAPI actúe como una "capa de lectura rápida" sobre la misma base de datos que Django administra, eliminando la necesidad de sincronizar dos esquemas de base de datos diferentes.   &lt;/p&gt;

&lt;h2&gt;
  
  
  Orquestación Profesional con Docker Compose
&lt;/h2&gt;

&lt;p&gt;La validación de esta tesis arquitectónica requiere una configuración de Docker que garantice la comunicación fluida entre servicios. Se prioriza el uso de redes aisladas y volúmenes persistentes para la base de datos compartida.   &lt;/p&gt;

&lt;h3&gt;
  
  
  Ejemplo de docker-compose.yml
&lt;/h3&gt;

&lt;p&gt;Este archivo define un ecosistema donde Django gestiona la base de datos y FastAPI consume esos datos para la API pública.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.9'&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;soberano_db&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;super_secret_password&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres_data:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;backend_net&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

  &lt;span class="na"&gt;django_core&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
      &lt;span class="s"&gt;context:.&lt;/span&gt;
      &lt;span class="s"&gt;dockerfile&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile.django&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python manage.py runserver 0.0.0.0:8000&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="s"&gt;-.:/code&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DATABASE_URL=postgres://dev:super_secret_password@database:5432/soberano_db&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;backend_net&lt;/span&gt;

  &lt;span class="na"&gt;fastapi_edge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="s"&gt;context:.&lt;/span&gt;
      &lt;span class="s"&gt;dockerfile&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile.fastapi&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uvicorn api_fastapi.main:app --host 0.0.0.0 --port &lt;/span&gt;&lt;span class="m"&gt;8080&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DATABASE_URL=postgres://dev:super_secret_password@database:5432/soberano_db&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DJANGO_SETTINGS_MODULE=core_django.settings&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;backend_net&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;backend_net&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este diseño garantiza que tanto Django como FastAPI compartan la misma "fuente de verdad" (PostgreSQL), pero operen en procesos y puertos distintos (8000 para administración, 8080 para la API pública), permitiendo un escalado independiente y una seguridad de red robusta.   &lt;/p&gt;

&lt;h2&gt;
  
  
  Seguridad y Cumplimiento: Comparativa de Enfoques
&lt;/h2&gt;

&lt;p&gt;La seguridad es un área donde la elección del framework dictamina la carga de trabajo del desarrollador. Django ofrece una seguridad "pasiva" o por defecto, mientras que FastAPI requiere una seguridad "activa" o explícita.   &lt;/p&gt;

&lt;h3&gt;
  
  
  El enfoque de Django: Protección contra el error humano
&lt;/h3&gt;

&lt;p&gt;Django está diseñado para proteger al desarrollador de sí mismo. Sus protecciones contra inyección SQL, CSRF y XSS están activadas por defecto y son difíciles de desactivar por accidente. En 2026, la madurez de su middleware de autenticación lo hace ideal para aplicaciones que manejan datos sensibles, como fintech o registros de salud, donde el cumplimiento normativo es estricto.   &lt;/p&gt;

&lt;h3&gt;
  
  
  El enfoque de FastAPI: Control total y responsabilidad
&lt;/h3&gt;

&lt;p&gt;FastAPI no asume nada. Aunque proporciona utilidades de seguridad excelentes en fastapi.security, la implementación real de la lógica de autenticación (hashing de contraseñas, validación de JWT, expiración de tokens) recae en el desarrollador. Esto ofrece una flexibilidad inigualable: es sencillo integrar proveedores de identidad externos como Auth0, Clerk o Supabase, pero aumenta la superficie de ataque si el desarrollador no sigue las mejores prácticas de seguridad.   &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Riesgo de Seguridad&lt;/th&gt;
&lt;th&gt;Mitigación en Django 6.0&lt;/th&gt;
&lt;th&gt;Mitigación en FastAPI 2026&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Inyección SQL&lt;/td&gt;
&lt;td&gt;ORM con parámetros saneados por defecto&lt;/td&gt;
&lt;td&gt;Pydantic para tipos + SQLModel/ORM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-Site Scripting (XSS)&lt;/td&gt;
&lt;td&gt;Auto-escape en templates + CSP nativo&lt;/td&gt;
&lt;td&gt;El desarrollador debe sanear salidas HTML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broken Authentication&lt;/td&gt;
&lt;td&gt;Sistema de sesión y auth robusto integrado&lt;/td&gt;
&lt;td&gt;OAuth2/JWT manual con dependencias&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broken Access Control&lt;/td&gt;
&lt;td&gt;Decoradores permission_required y RBAC&lt;/td&gt;
&lt;td&gt;Scopes de OAuth2 y Dependency Injection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Rendimiento y Escalabilidad: El veredicto de los datos
&lt;/h2&gt;

&lt;p&gt;Para un arquitecto de software, el rendimiento no es solo una métrica de vanidad; es una cuestión de costos operativos. La eficiencia de FastAPI permite que una aplicación maneje el mismo tráfico que Django utilizando un 30-50% menos de recursos de cómputo en la nube.   &lt;/p&gt;

&lt;h3&gt;
  
  
  Concurrencia Asíncrona vs. Multihilo
&lt;/h3&gt;

&lt;p&gt;La gran diferencia radica en cómo manejan las conexiones. Django, a pesar de sus mejoras asíncronas, todavía depende en gran medida de un modelo de un hilo por petición cuando se usa WSGI. FastAPI, siendo nativo ASGI y asíncrono, utiliza un bucle de eventos (event loop) que permite pausar una tarea mientras espera a la base de datos y atender otra petición en el ínterin.   &lt;/p&gt;

&lt;p&gt;Esta característica es la que permite que FastAPI maneje miles de conexiones de WebSockets simultáneas, algo que en Django tradicional requeriría una configuración compleja con Django Channels, añadiendo una carga significativa al servidor.   &lt;/p&gt;

&lt;h3&gt;
  
  
  Consumo de Memoria y Cold Starts
&lt;/h3&gt;

&lt;p&gt;En arquitecturas serverless (como AWS Lambda o Google Cloud Run), el tiempo de inicio y la memoria base son vitales. FastAPI tiene una huella de memoria inicial de aproximadamente 15-20 MB, mientras que un proyecto Django estándar con varias aplicaciones instaladas puede superar fácilmente los 80-100 MB. Esto hace que FastAPI sea mucho más eficiente para funciones de pago por uso o microservicios que se apagan y encienden según la demanda.   &lt;/p&gt;

&lt;h2&gt;
  
  
  Futuro del ecosistema: Hacia Python 3.15 y más allá
&lt;/h2&gt;

&lt;p&gt;Mirando hacia el futuro cercano, las tendencias indican una convergencia aún mayor. Django continuará "asincronizando" sus componentes internos, mientras que FastAPI verá el surgimiento de más "baterías" comunitarias que emulen el panel de administración de Django (como el proyecto &lt;a href="https://github.com/Oppkey/fastopp" rel="noopener noreferrer"&gt;FastOpp&lt;/a&gt;).   &lt;/p&gt;

&lt;h3&gt;
  
  
  IA e Integración de Modelos
&lt;/h3&gt;

&lt;p&gt;El papel de Python en la IA seguirá impulsando a FastAPI como la opción preferida para servir modelos de lenguaje y agentes autónomos. La capacidad de FastAPI para manejar flujos de datos (streaming responses) es perfecta para las respuestas palabra por palabra de los chatbots modernos. Django, por su parte, se posicionará como la plataforma de gestión para estos sistemas de IA, permitiendo a los humanos supervisar, corregir y auditar los datos que alimentan a los modelos.   &lt;/p&gt;

&lt;h3&gt;
  
  
  El rol de los desarrolladores en 2026
&lt;/h3&gt;

&lt;p&gt;Para los desarrolladores Python, la especialización en un solo framework ya no es suficiente. El mercado demanda profesionales que entiendan la arquitectura de sistemas: saber cuándo usar la solidez de Django para un proceso administrativo y cuándo desplegar la velocidad de FastAPI para una API crítica. La capacidad de orquestar ambos mundos con Docker y asegurar la comunicación mediante estándares modernos es la habilidad que define al desarrollador senior en esta era.   &lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusión: La síntesis del poder y la velocidad
&lt;/h2&gt;

&lt;p&gt;En 2026, la respuesta a la pregunta "¿Django o FastAPI?" es un rotundo "Ambos". La evolución de Django 6.0 ha demostrado que el framework veterano aún tiene mucho que ofrecer, especialmente con su nuevo sistema de tareas y seguridad CSP integrada. FastAPI, por su parte, ha madurado hasta convertirse en una herramienta de precisión quirúrgica para el rendimiento y la escalabilidad asíncrona.   &lt;/p&gt;

&lt;p&gt;Al adoptar una arquitectura híbrida, los desarrolladores no están haciendo un compromiso, sino una optimización estratégica:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.djangoproject.com/" rel="noopener noreferrer"&gt;Django&lt;/a&gt;&lt;/strong&gt; proporciona la paz mental de una seguridad probada y una administración instantánea para el equipo interno.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://fastapi.tiangolo.com/" rel="noopener noreferrer"&gt;FastAPI&lt;/a&gt;&lt;/strong&gt; garantiza una experiencia de usuario fluida con latencias mínimas y la capacidad de escalar hasta el infinito en la nube.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.docker.com/" rel="noopener noreferrer"&gt;Docker&lt;/a&gt;&lt;/strong&gt; actúa como el pegamento que permite que estas dos potencias coexistan en una infraestructura limpia, mantenible y profesional.
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Elegir ambos es elegir no tener que sacrificar la productividad por el rendimiento. Es, en última instancia, la marca de un arquitecto que comprende que las herramientas están al servicio de la solución, y no al revés. En el dinámico mundo de Python en 2026, la versatilidad es la verdadera clave del éxito.&lt;/p&gt;

&lt;h2&gt;
  
  
  Referencias
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://codism.io/django-vs-fastapi-when-to-use-each-in-2026/" rel="noopener noreferrer"&gt;Django vs FastAPI: When to Use Each in 2026
&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dzone.com/articles/django-architecture-versus-fastapi" rel="noopener noreferrer"&gt;Django Architecture vs FastAPI: A Learning Path
&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.scaler.com/blog/python-roadmap-six-month-path-to-master-core-ai-web-dev/" rel="noopener noreferrer"&gt;Python Roadmap 2026: 6-Month Path to Master Core, AI &amp;amp; Web Dev
&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pysquad.com/blogs/why-fastapi-is-the-go-to-choice-for-high-performance-apis-in-2025" rel="noopener noreferrer"&gt;Why FastAPI is the Go-To Choice for High-Performance APIs in 2026
&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>django</category>
      <category>fastapi</category>
      <category>docker</category>
    </item>
    <item>
      <title>De Localhost a Escala Global: Una Estrategia de Arquitectura AWS para Emprendedores en Latinoamérica</title>
      <dc:creator>Carlos Eduardo Sotelo Pinto</dc:creator>
      <pubDate>Fri, 09 Jan 2026 12:42:59 +0000</pubDate>
      <link>https://dev.to/csotelo/de-localhost-a-escala-global-una-estrategia-de-arquitectura-aws-para-emprendedores-en-latinoamerica-3e7i</link>
      <guid>https://dev.to/csotelo/de-localhost-a-escala-global-una-estrategia-de-arquitectura-aws-para-emprendedores-en-latinoamerica-3e7i</guid>
      <description>&lt;p&gt;La ciudad de Arequipa, cuna de juristas y pensadores, se encuentra hoy ante una encrucijada histórica. Históricamente definida por su comercio textil, su industria minera y su agricultura de exportación, la "Ciudad Blanca" está experimentando una metamorfosis silenciosa pero tectónica hacia la economía del conocimiento. Bajo la vigilancia de los volcanes Misti, Chachani y Pichu Pichu, una nueva generación de arquitectos de software, ingenieros de datos y fundadores de startups está redefiniendo lo que significa emprender desde el sur del Perú. Sin embargo, este renacimiento digital se enfrenta a un desafío fundamental: la brecha entre la concepción de una idea en un entorno de desarrollo local ("localhost") y la realidad operativa de desplegar una aplicación capaz de escalar globalmente.&lt;/p&gt;

&lt;p&gt;Para el emprendedor latinoamericano, y específicamente para el arequipeño, el camino hacia la nube no es simplemente una decisión técnica; es una maniobra de supervivencia económica. A diferencia de sus contrapartes en Silicon Valley o Londres, nuestros fundadores operan en un entorno caracterizado por un acceso limitado al capital de riesgo en etapas tempranas, una infraestructura física local costosa y vulnerable, y una logística de importación tecnológica que a menudo roza lo kafkiano. En este contexto, Amazon Web Services (AWS) no se presenta solo como un proveedor de servicios de infraestructura, sino como el gran nivelador que democratiza el acceso a la potencia computacional de clase mundial.&lt;/p&gt;

&lt;p&gt;Este informe técnico, se aleja de la retórica convencional de "migrar por migrar". En su lugar, propone una tesis arquitectónica pragmática y evolutiva. Rechazamos la noción de que una startup debe iniciar con arquitecturas de microservicios complejas y costosas desde el día uno. Por el contrario, introducimos una estrategia basada en la madurez progresiva, identificando a AWS Lightsail como el "eslabón perdido" estratégico que permite a las empresas validar sus modelos de negocio con un riesgo financiero mínimo, antes de evolucionar hacia servicios más granulares y potentes como Amazon EC2, Amazon S3, Amazon RDS y AWS Lambda.   &lt;/p&gt;

&lt;p&gt;A través de un análisis profundo que abarca desde la microeconomía del hardware en Perú hasta los patrones de diseño de redes híbridas en la nube, trazaremos una hoja de ruta técnica que honra tanto la realidad de nuestros presupuestos como la ambición de nuestra ingeniería.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Tesis Económica para LATAM: La Muerte del CAPEX y la Tiranía de la Latencia
&lt;/h2&gt;

&lt;p&gt;Para comprender por qué la arquitectura en la nube es una necesidad existencial y no una opción de lujo para las startups en regiones como Arequipa, debemos diseccionar primero la realidad económica de la infraestructura tradicional en nuestra región. La decisión de dónde y cómo ejecutar el código tiene implicaciones financieras directas que pueden determinar la solvencia de una empresa emergente en sus primeros 18 meses de vida.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.1. La Trampa del Hardware: Anatomía de una Importación en el Perú
&lt;/h3&gt;

&lt;p&gt;En los modelos de gestión financiera tradicionales, la infraestructura de Tecnologías de la Información (TI) se clasifica predominantemente como CAPEX (&lt;em&gt;Capital Expenditure&lt;/em&gt; o Gastos de Capital). Esto implica una inversión inicial masiva para adquirir activos físicos—servidores, racks, sistemas de refrigeración, licencias perpetuas—que se deprecian contablemente a lo largo de varios años. Para una empresa establecida en mercados con cadenas de suministro maduras, esto puede ser una gestión de activos rutinaria. Para una startup en Perú, representa una barrera de entrada formidable.&lt;/p&gt;

&lt;p&gt;Analicemos la odisea financiera y logística de importar un servidor de base de datos de nivel empresarial a Perú en el contexto actual de 2025. El proceso está plagado de costos hundidos y fricciones burocráticas que drenan la liquidez, el recurso más preciado de cualquier emprendimiento:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Valor FOB (Free On Board):&lt;/strong&gt; El precio base del hardware en el puerto de origen (usualmente China o Estados Unidos).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flete y Seguro Internacional:&lt;/strong&gt; Costos logísticos que, aunque se han estabilizado tras las crisis de cadenas de suministro de años anteriores, siguen representando un porcentaje significativo del costo total, especialmente para equipos delicados de alta tecnología.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Valor CIF (Cost, Insurance, and Freight):&lt;/strong&gt; La suma del valor FOB, el flete y el seguro, que constituye la base imponible para el cálculo de los tributos aduaneros en Perú.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Derechos Ad Valorem:&lt;/strong&gt; Dependiendo de la subpartida nacional específica del hardware, la importación puede estar sujeta a una tasa arancelaria que varía entre el 0%, 9% y 17%. Aunque Perú cuenta con Tratados de Libre Comercio (TLC) con potencias tecnológicas como EE.UU. y China que pueden reducir este arancel a cero, la correcta acreditación del origen y la conformidad técnica requieren a menudo la intervención de agentes de aduana especializados, añadiendo costos administrativos.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Impuesto General a las Ventas (IGV) e Impuesto de Promoción Municipal (IPM):&lt;/strong&gt; Aquí reside el golpe más duro a la caja chica de la startup. La SUNAT aplica un 16% de IGV más un 2% de IPM sobre la suma del Valor CIF y los derechos Ad Valorem. Esto significa que el emprendedor debe desembolsar un 18% adicional de efectivo antes de que el servidor haya procesado una sola transacción o generado un solo sol de ingreso.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Costos de Nacionalización y Percepción:&lt;/strong&gt; Además de los impuestos base, existen costos de almacenaje en depósitos temporales, transporte local y, en muchos casos, el Régimen de Percepción del IGV, que obliga a un pago adelantado adicional de impuestos (generalmente 3.5% o 10% para importadores nuevos).
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Más allá de los costos de adquisición, la operación de infraestructura on-premise en una geografía como Arequipa conlleva riesgos operativos existenciales. La ciudad se encuentra en una zona de alta actividad sísmica. Un sismo de magnitud moderada no solo amenaza la integridad física de los discos duros giratorios, sino que puede interrumpir el suministro eléctrico y la conectividad de fibra óptica durante horas. Para mitigar esto, se requieren inversiones adicionales en UPS (Sistemas de Alimentación Ininterrumpida), generadores diésel y sistemas de refrigeración redundantes, convirtiendo una oficina de startup en un mini centro de datos difícil de mantener.   &lt;/p&gt;

&lt;h3&gt;
  
  
  2.2. OPEX como Instrumento de Liberación Financiera
&lt;/h3&gt;

&lt;p&gt;La adopción de la nube, y específicamente de AWS, transforma radicalmente este modelo hacia el OPEX (Operational Expenditure o Gastos Operativos). Esta transición no es meramente contable; es estratégica. Al consumir infraestructura como servicio, las startups peruanas cambian grandes desembolsos de capital por costos variables que se alinean directamente con el uso y, idealmente, con los ingresos.   &lt;/p&gt;

&lt;p&gt;Para un fundador en Arequipa, las ventajas del modelo OPEX son tangibles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Preservación de Liquidez:&lt;/strong&gt; El capital semilla, a menudo escaso en el ecosistema local, se puede destinar a la contratación de talento, desarrollo de producto y adquisición de usuarios, en lugar de inmovilizarse en activos depreciables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agilidad Fiscal y Contable:&lt;/strong&gt; Los servicios de nube se facturan mensualmente. Estos costos se registran como gastos operativos en el mismo periodo fiscal en que se incurren, lo que simplifica la gestión tributaria y permite deducir el gasto del Impuesto a la Renta de manera inmediata, a diferencia de la depreciación de activos fijos que toma años en reflejarse en los libros.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eliminación del Riesgo Tecnológico:&lt;/strong&gt; En un entorno on-premise, el hardware comprado hoy es obsoleto en tres años. En AWS, la responsabilidad de la actualización del hardware recae en el proveedor. La startup siempre tiene acceso a la última generación de procesadores y almacenamiento sin costo de cambio.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2.3. El Impuesto Digital 2025: Navegando la Nueva Realidad Tributaria
&lt;/h3&gt;

&lt;p&gt;Es imperativo abordar con precisión el panorama tributario actual en Perú, especialmente tras las modificaciones legislativas introducidas a finales de 2024 y vigentes en 2025, conocidas coloquialmente como la "Tasa Netflix", pero que afectan a todos los servicios digitales transfronterizos, incluyendo la infraestructura en la nube.   &lt;/p&gt;

&lt;p&gt;El &lt;a href="https://busquedas.elperuano.pe/dispositivo/NL/2312442-1" rel="noopener noreferrer"&gt;Decreto Legislativo N° 1623&lt;/a&gt; ha establecido mecanismos para la recaudación del IGV en la utilización de servicios digitales prestados por sujetos no domiciliados. La aplicación de este impuesto varía críticamente según la naturaleza del cliente:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Escenario&lt;/th&gt;
&lt;th&gt;Tipo de Cliente&lt;/th&gt;
&lt;th&gt;Implicancia Tributaria con AWS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;B2C (Business to Consumer)&lt;/td&gt;
&lt;td&gt;Personas naturales sin negocio / Hobbistas&lt;/td&gt;
&lt;td&gt;AWS, actuando como agente de percepción o retención, está obligado a cobrar el 18% de IGV directamente en la factura o a través de la tarjeta de crédito del usuario. Esto encarece el servicio en un 18% para desarrolladores individuales que no han formalizado su actividad.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B2B (Business to Business)&lt;/td&gt;
&lt;td&gt;Startups y Empresas con RUC activo&lt;/td&gt;
&lt;td&gt;Las empresas peruanas deben registrar su número de RUC en la consola de facturación de AWS (&lt;em&gt;Tax Settings&lt;/em&gt;). En este escenario, bajo el mecanismo de "Inversión del Sujeto Pasivo" (&lt;em&gt;Reverse Charge&lt;/em&gt;), AWS generalmente &lt;em&gt;no&lt;/em&gt; cobra el IGV en la factura comercial. Es responsabilidad de la empresa peruana autoliquidar el "IGV de No Domiciliados" ante la SUNAT mediante el Formulario Virtual 617 u otros medios designados.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Insight Estratégico:&lt;/em&gt; Para una startup que busca escalar, la formalización (obtención del RUC y régimen tributario adecuado) es el primer paso de optimización de costos en la nube. Operar como B2B permite gestionar el IGV como crédito fiscal (dependiendo del régimen), mientras que operar como persona natural (B2C) convierte ese 18% en un costo hundido no recuperable. Además, la correcta identificación tributaria es vital para evitar la doble imposición o retenciones indebidas por parte de los bancos locales.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.4. La Geopolítica de la Latencia: ¿Virginia, Ohio o São Paulo?
&lt;/h3&gt;

&lt;p&gt;Una pregunta recurrente en los meetups del AWS User Group Arequipa es sobre la selección de la región: "Si estamos en Sudamérica, ¿por qué no desplegamos todo en la región de São Paulo (sa-east-1)?" La respuesta requiere un análisis de la infraestructura global de internet y la economía de precios de AWS.&lt;/p&gt;

&lt;p&gt;A pesar de la proximidad geográfica, la región de São Paulo presenta dos desventajas críticas para una startup peruana en etapa temprana:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sobrecosto Estructural:&lt;/strong&gt; Debido a la compleja carga tributaria y los altos costos de importación y energía en Brasil, los servicios en sa-east-1 son consistentemente más caros (a menudo entre un 30% y un 50% más) que en las regiones de Estados Unidos. Para una startup que cuida cada dólar, este sobreprecio es difícil de justificar sin un requisito estricto de residencia de datos.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Topología de Red:&lt;/strong&gt; La conectividad de internet de Perú depende en gran medida de cables submarinos como el SAm-1 y el Curie que conectan la costa del Pacífico (Lurín) hacia el norte, con puntos de aterrizaje en Panamá y Florida (Miami). El tráfico desde Arequipa hacia Brasil a menudo no cruza el continente directamente a través de la selva o los Andes; en muchos casos, viaja hasta Miami y luego "baja" a São Paulo. Esto resulta en que la latencia hacia us-east-1 (N. Virginia) suele oscilar entre 110ms y 140ms, muy competitiva e incluso a veces inferior a la ruta hacia Brasil dependiendo del ISP.
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Veredicto:&lt;/strong&gt; Para el 90% de las startups en Arequipa, la región &lt;strong&gt;US East (N. Virginia)&lt;/strong&gt; &lt;code&gt;us-east-1&lt;/code&gt; (o sus alternativas en Ohio &lt;code&gt;us-east-2&lt;/code&gt; u Oregón &lt;code&gt;us-west-2&lt;/code&gt;) ofrece el punto óptimo de equilibrio: los precios más bajos del mercado global, acceso prioritario a nuevas funcionalidades de AWS y una latencia totalmente aceptable para aplicaciones web y móviles estándar. La región de Sudamérica se debe reservar para casos de uso específicos de fintech regulada o aplicaciones de tiempo real crítico donde cada milisegundo cuenta y el presupuesto lo permite.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. El Punto de Entrada "Sin Excusas": AWS Lightsail
&lt;/h2&gt;

&lt;p&gt;En la literatura técnica y las certificaciones de arquitectura, a menudo se nos enseña el "camino purista": diseñar una Virtual Private Cloud (VPC) desde cero, segmentar subredes públicas y privadas, configurar NAT Gateways para la salida segura a internet, desplegar Application Load Balancers (ALB) y configurar Auto Scaling Groups. Si bien este diseño es robusto y escalable , presenta un defecto fatal para una startup en fase de validación: &lt;strong&gt;Complejidad cognitiva y costos fijos elevados.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Un solo NAT Gateway en AWS cuesta aproximadamente $0.045 por hora, más el procesamiento de datos. Esto se traduce en unos $32 USD mensuales por zona de disponibilidad, solo por tener la "capacidad" de que las instancias privadas salgan a internet. Sumando un ALB (~$16 USD + LCUs) y las instancias EC2, la factura puede superar los $100 USD mensuales antes de recibir el primer cliente.   &lt;/p&gt;

&lt;p&gt;Aquí es donde introducimos &lt;strong&gt;AWS Lightsail&lt;/strong&gt; no como una herramienta para principiantes, sino como un movimiento estratégico deliberado.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1. Lightsail: El VPS Plug-and-Play Disruptivo
&lt;/h3&gt;

&lt;p&gt;AWS Lightsail es, en esencia, un servicio de Servidor Privado Virtual (VPS) gestionado que encapsula la potencia de AWS en una experiencia simplificada. Pero su valor real para el emprendedor no es solo la simplicidad, sino la predictibilidad financiera.&lt;/p&gt;

&lt;p&gt;A diferencia del modelo on-demand puro de EC2 donde se paga por cómputo, almacenamiento y transferencia por separado, Lightsail ofrece "paquetes" (bundles) a precio fijo. Por una tarifa mensual (ej. $3.50, $5, $10 USD), una startup obtiene :   &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cómputo:&lt;/strong&gt; Una instancia virtual (basada en la tecnología de la familia T de EC2, con capacidad de ráfaga o burst).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Almacenamiento:&lt;/strong&gt; Un disco SSD persistente de tamaño generoso (ej. 20GB, 40GB).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Networking:&lt;/strong&gt; Una dirección IP estática incluida y, lo más importante, una asignación masiva de transferencia de datos de salida (ej. 1 TB a 5 TB mensuales).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Esta inclusión de la transferencia de datos es el "asesino de costos". En EC2, la transferencia de salida cuesta aproximadamente $0.09 USD por GB. Un terabyte de tráfico en EC2 costaría unos $90 USD solo en red. En Lightsail, ese mismo terabyte viene incluido en un plan de $5 USD. Para una startup de contenido, streaming o e-commerce, esta diferencia es abismal.   &lt;/p&gt;

&lt;h3&gt;
  
  
  3.2. La Estrategia del "Monolito Glorioso"
&lt;/h3&gt;

&lt;p&gt;En la etapa inicial de una startup, la velocidad de iteración supera a la pureza arquitectónica. No necesitamos microservicios orquestados en Kubernetes; necesitamos validar que el mercado quiere nuestro producto. Lightsail permite desplegar un "Monolito Glorioso": toda la pila tecnológica (servidor web, lógica de negocio, base de datos) en una sola instancia.&lt;/p&gt;

&lt;p&gt;Utilizando los Blueprints de Bitnami integrados en Lightsail, un desarrollador en Arequipa puede lanzar un stack LAMP (Linux, Apache, MySQL, PHP), MEAN, Node.js, Django o WordPress en cuestión de minutos. Esto reduce el Time-to-Market drásticamente. Además, con el Free Tier de AWS (que a menudo incluye 3 meses gratuitos en ciertos planes de Lightsail), el costo de validación es efectivamente cero.   &lt;/p&gt;

&lt;h3&gt;
  
  
  3.3. El Eslabón Perdido: VPC Peering (La Puerta Trasera al Poder)
&lt;/h3&gt;

&lt;p&gt;Muchos arquitectos descartan Lightsail porque lo ven como un "jardín vallado", aislado del ecosistema profundo de AWS. Esta es una concepción errónea que nuestra estrategia explota. Lightsail reside en una VPC gestionada por AWS "en la sombra", pero posee una capacidad crítica: el VPC Peering (Emparejamiento de VPC).&lt;/p&gt;

&lt;p&gt;El VPC Peering permite conectar la VPC oculta de Lightsail con la VPC Por Defecto (Default VPC) de la cuenta de AWS en la misma región. Al activar esta función (un simple checkbox en la consola de Lightsail), ocurre la magia:   &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Visibilidad de Red:&lt;/strong&gt; La instancia Lightsail puede "ver" y comunicarse con recursos desplegados en la Default VPC (como instancias RDS, ElastiCache o interfaces de red privadas).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comunicación Privada:&lt;/strong&gt; La comunicación ocurre a través de direcciones IP privadas, lo que mejora la seguridad al no exponer tráfico a la internet pública.
3- &lt;strong&gt;Economía de Transferencia:&lt;/strong&gt; La transferencia de datos entre Lightsail y los recursos en la Default VPC (dentro de la misma región) es generalmente gratuita o de costo muy reducido, tratándose como tráfico interno de AWS.
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Advertencia Técnica Crítica:&lt;/strong&gt; Lightsail tiene una limitación "dura": solo puede hacer peering con la VPC Por Defecto. No se puede configurar peering con una VPC personalizada creada por el usuario. Esto es vital para la planificación. Si una startup, siguiendo un tutorial avanzado, elimina su Default VPC para "empezar de cero", perderá la capacidad de integrar Lightsail con el resto de AWS fácilmente (aunque se puede solicitar a soporte que se recree la Default VPC). La estrategia aquí es: No borren su Default VPC. Úsenla como el muelle de conexión para su flota de Lightsail.   &lt;/p&gt;

&lt;h2&gt;
  
  
  4. Arquitectura para la Madurez: La Evolución Paso a Paso
&lt;/h2&gt;

&lt;p&gt;El éxito trae consigo nuevos problemas: el tráfico aumenta, la base de datos local del monolito se satura y el disco duro se llena de uploads de usuarios. En lugar de una reingeniería total y costosa ("Big Bang"), nuestra estrategia propone una descomposición progresiva. Desmantelamos el monolito pieza por pieza, reemplazando componentes internos de Lightsail con servicios nativos de AWS, aprovechando el puente del VPC Peering que ya hemos establecido.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1. Fase 1: Desacoplar el Almacenamiento (Amazon S3)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;El Problema:&lt;/strong&gt; El disco SSD de una instancia Lightsail es rápido pero finito. Si nuestra aplicación (por ejemplo, una plataforma de e-commerce para artesanías de Arequipa) permite subir imágenes de alta resolución, el disco se llenará rápidamente. Además, el almacenamiento local es efímero en caso de fallo catastrófico de la instancia; si el servidor se corrompe, los datos de usuario mueren con él.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La Solución:&lt;/strong&gt; Migrar el almacenamiento de archivos estáticos y media a &lt;strong&gt;Amazon S3 (Simple Storage Service)&lt;/strong&gt;. S3 ofrece durabilidad del 99.999999999% ("once nueves") y capacidad infinita teórica.   &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementación Técnica:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Refactorización Ligera:&lt;/strong&gt; Modificamos la aplicación (en Python/Django, PHP/Laravel, Node/Express) para utilizar el AWS SDK (como Boto3). En lugar de guardar archivos en el sistema de archivos local (/var/www/html/uploads), los enviamos directamente a un bucket S3.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Gestión de Credenciales (El Reto de Seguridad):&lt;/strong&gt; Aquí encontramos una limitación de Lightsail: a diferencia de EC2, Lightsail no soporta nativamente los "Instance Profiles" (Roles IAM) que rotan credenciales automáticamente.   &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Práctica Recomendada:&lt;/strong&gt; Crear un Usuario IAM específico con una política estricta que solo permita s3:PutObject y s3:GetObject en el bucket específico de la aplicación. Generar Access Keys para este usuario y configurarlas como variables de entorno en la instancia Lightsail o mediante el archivo ~/.aws/credentials. Nunca hardcodear las claves en el código fuente.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Economía de Datos S3-Lightsail:&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;La transferencia de datos hacia S3 (subida) es gratuita.&lt;/li&gt;
&lt;li&gt;La transferencia desde S3 hacia Lightsail es un punto de confusión común. Según la documentación oficial, si se utilizan IPs Privadas, la transferencia entre servicios de AWS en la misma región es gratuita. Sin embargo, S3 es un servicio público por defecto. Para maximizar la eficiencia, se debe interactuar con S3 dentro de la misma región (us-east-1 a us-east-1). Aunque S3 se accede técnicamente por endpoints públicos, AWS no cobra la transferencia de datos saliente desde S3 hacia EC2/Lightsail en la misma región.
&lt;/li&gt;
&lt;li&gt;Para servir el contenido a los usuarios finales, se puede configurar una distribución CDN de Lightsail (que usa CloudFront por debajo) apuntando al bucket S3, aprovechando la capa gratuita de CDN de Lightsail (ej. 1 año de 50GB/mes).
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4.2. Fase 2: Desacoplar la Base de Datos (Amazon RDS)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;El Problema:&lt;/strong&gt; La base de datos local (MySQL/PostgreSQL) compite brutalmente por la memoria RAM y la CPU con el servidor web (Apache/Nginx) dentro de la misma instancia Lightsail. Un pico de tráfico web puede causar que el proceso de base de datos sea eliminado por el sistema operativo (OOM Killer), tirando abajo todo el servicio. Además, la gestión manual de backups y parches es un riesgo operativo inaceptable a medida que se escala.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La Solución:&lt;/strong&gt; Migrar a Amazon RDS (Relational Database Service).   &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementación Técnica:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Despliegue Estratégico:&lt;/strong&gt; Aunque Lightsail ofrece "Bases de Datos Gestionadas" (una versión simplificada de RDS), la recomendación estratégica es desplegar una instancia Amazon RDS nativa directamente en la Default VPC.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;¿Por qué?&lt;/em&gt; RDS nativo ofrece mayor granularidad de configuración, acceso a tipos de instancias más variados (incluyendo Graviton para mejor costo/rendimiento) y, crucialmente, nos prepara para cuando abandonemos Lightsail por completo. El costo puede ser similar o ligeramente menor si se usan instancias reservadas en el futuro.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Conectividad vía Peering (El Paso Crítico):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Asegurar que el VPC Peering esté activo entre Lightsail y la Default VPC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configuración de Security Groups:&lt;/strong&gt; Este es el punto donde la mayoría falla. En el Security Group de la instancia RDS (en la Default VPC), debemos añadir una regla de entrada (Inbound Rule) para el puerto de la base de datos (3306 para MySQL, 5432 para PostgreSQL).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El Truco del CIDR:&lt;/strong&gt; Como no podemos referenciar "lógicamente" el Security Group de Lightsail dentro del Security Group de la VPC, debemos autorizar la IP Privada específica de nuestra instancia Lightsail. Dado que las IPs privadas en Lightsail son estáticas durante la vida de la instancia, esto es seguro y efectivo. Alternativamente, si tenemos varias instancias, podemos autorizar el bloque CIDR completo de la VPC de Lightsail (visible en la consola de Peering), aunque esto es menos restrictivo.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Migración de Datos:&lt;/strong&gt; Utilizamos herramientas estándar como mysqldump o pg_dump para exportar la data local e importarla en el endpoint del RDS a través de la conexión privada.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Resultado:&lt;/strong&gt; La base de datos ahora vive en un entorno dedicado, con backups automáticos (Point-in-Time Recovery) y la capacidad de escalar verticalmente (cambiar el tamaño de instancia) o activar Multi-AZ (alta disponibilidad) con un clic, sin afectar al servidor web.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  4.3. Fase 3: Desacoplar la Lógica y Tareas Programadas (AWS Lambda y EventBridge)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;El Problema:&lt;/strong&gt; Los Cron jobs (tareas programadas) en el servidor monolítico son ineficientes. Procesos como el envío masivo de correos, la generación de reportes PDF nocturnos o el redimensionamiento de imágenes consumen ciclos de CPU valiosos que deberían dedicarse a servir peticiones de usuarios. Si la instancia web se cae o se reinicia, los trabajos programados fallan silenciosamente.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La Solución:&lt;/strong&gt; Computación Serverless con AWS Lambda orquestada por Amazon EventBridge.   &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementación Técnica:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reemplazo del Cron:&lt;/strong&gt; En lugar de mantener un fichero crontab en Linux, configuramos una regla en &lt;strong&gt;Amazon EventBridge Scheduler&lt;/strong&gt;. Esta regla se dispara según una expresión cron (ej. cron(0 8? * MON-FRI *)) e invoca una función Lambda.
&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;La función Lambda contiene la lógica de negocio (ej. conectarse al RDS, consultar usuarios inactivos y enviar un email a través de Amazon SES).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Ventaja Económica:&lt;/em&gt; Una función Lambda que corre 2 minutos al día cuesta fracciones de centavo. Un servidor EC2 dedicado para tareas batch costaría dinero las 24 horas del día, incluso cuando está inactivo.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Arquitectura Orientada a Eventos:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Para el procesamiento de imágenes, configuramos el bucket S3 para que emita una notificación de evento cada vez que se sube un objeto (s3:ObjectCreated:*).&lt;/li&gt;
&lt;li&gt;Este evento dispara una función Lambda asíncrona que descarga la imagen, la optimiza/redimensiona y la guarda en un bucket de destino o actualiza la base de datos.
&lt;/li&gt;
&lt;li&gt;Esto elimina completamente la carga de procesamiento de medios del servidor Lightsail, permitiéndole manejar más usuarios concurrentes con la misma capacidad de CPU.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4.4. Fase 4: La Graduación (El Éxodo a EC2)
&lt;/h3&gt;

&lt;p&gt;Llegará el momento en que el éxito de la startup supere las capacidades de Lightsail. Quizás se necesite Auto Scaling real para manejar picos de tráfico durante campañas como el Cyber Wow, o se requieran tipos de instancias especializadas (Familia C para cómputo intensivo, Familia R para memoria) que no existen en el menú de Lightsail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El Camino de Salida (Upgrade Path):&lt;/strong&gt; Aquí es donde la elección de AWS brilla sobre otros proveedores de VPS. No estamos encerrados. AWS ofrece una ruta de migración nativa y fluida.   &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot y Exportación:&lt;/strong&gt; Desde la consola de Lightsail, tomamos una instantánea (Snapshot) de la instancia. Usamos la función "Export to EC2".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Creación de AMI:&lt;/strong&gt; AWS toma ese snapshot y crea una AMI (Amazon Machine Image) y un volumen EBS en la consola de EC2.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lanzamiento Empresarial:&lt;/strong&gt; Usamos esa AMI para lanzar nuevas instancias EC2 dentro de una arquitectura VPC "madura": subredes privadas, Application Load Balancers (ALB), NAT Gateways y Auto Scaling Groups.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Continuidad:&lt;/strong&gt; Como ya habíamos desacoplado la base de datos (a RDS) y el almacenamiento (a S3) en las fases anteriores, la migración del servidor web es trivial y sin pérdida de datos.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  5. Análisis de Costos Comparativo
&lt;/h2&gt;

&lt;p&gt;Para el emprendedor, la visualización del ahorro es vital. A continuación, presentamos una tabla comparativa de costos mensuales estimados para una arquitectura típica de inicio (Servidor Web + Base de Datos pequeña + 1TB de transferencia), contrastando el enfoque tradicional de EC2 frente a la estrategia Lightsail propuesta.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Componente&lt;/th&gt;
&lt;th&gt;Arquitectura Tradicional (EC2 "Purista")&lt;/th&gt;
&lt;th&gt;Arquitectura Estratégica (Lightsail + RDS)&lt;/th&gt;
&lt;th&gt;Análisis de Impacto Económico&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cómputo Web&lt;/td&gt;
&lt;td&gt;t3.micro (On-Demand): ~$7.60/mes&lt;/td&gt;
&lt;td&gt;Lightsail Bundle (1GB RAM): $5.00/mes&lt;/td&gt;
&lt;td&gt;Lightsail es más barato y predecible.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Almacenamiento&lt;/td&gt;
&lt;td&gt;EBS gp3 (40GB): ~$3.20/mes&lt;/td&gt;
&lt;td&gt;Incluido en el bundle (40GB SSD)&lt;/td&gt;
&lt;td&gt;El almacenamiento está subsidiado en Lightsail.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Base de Datos&lt;/td&gt;
&lt;td&gt;RDS db.t3.micro (Single-AZ): ~$17.00/mes&lt;/td&gt;
&lt;td&gt;RDS db.t3.micro (en Default VPC): ~$17.00/mes&lt;/td&gt;
&lt;td&gt;El costo de RDS es idéntico, pero ganamos robustez.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transferencia de Datos&lt;/td&gt;
&lt;td&gt;1 TB Salida a Internet: ~$90.00 USD&lt;/td&gt;
&lt;td&gt;Incluido en el bundle (hasta 2 TB)&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;Diferencia Crítica.&lt;/em&gt; En EC2, la transferencia es el costo oculto más peligroso ($0.09/GB). En Lightsail, el primer TB es "gratis".&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IP Estática&lt;/td&gt;
&lt;td&gt;Gratis (si asociada)&lt;/td&gt;
&lt;td&gt;Gratis (si asociada)&lt;/td&gt;
&lt;td&gt;Igual.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Costo Total Estimado&lt;/td&gt;
&lt;td&gt;~$117.80 USD / mes&lt;/td&gt;
&lt;td&gt;~$22.00 USD / mes&lt;/td&gt;
&lt;td&gt;Ahorro del ~81%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;Nota:&lt;/strong&gt; Los precios son estimados basados en la región us-east-1 a Enero de 2026 y pueden variar. El ahorro masivo proviene de la inclusión de la transferencia de datos en el paquete de Lightsail, lo que actúa como un "escudo financiero" para la startup en crecimiento.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Conclusión: Construyendo el Futuro desde Arequipa
&lt;/h2&gt;

&lt;p&gt;La nube ha democratizado el acceso a la infraestructura tecnológica, pero la responsabilidad de utilizarla con sabiduría recae en nosotros. Para la comunidad tecnológica de Arequipa, el mensaje de este análisis es contundente: no existen excusas técnicas ni financieras válidas para no construir productos de clase mundial.&lt;/p&gt;

&lt;p&gt;Hemos demostrado que:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;El modelo CAPEX es obsoleto para la innovación ágil;&lt;/strong&gt; la importación de hardware es una carga innecesaria frente a la flexibilidad del OPEX en la nube.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Lightsail es el punto de partida ideal,&lt;/strong&gt; ofreciendo una protección financiera contra los costos de transferencia de datos y una simplicidad operativa que permite enfocarse en el producto.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El VPC Peering es la herramienta secreta&lt;/strong&gt; que permite una arquitectura híbrida, combinando la economía de Lightsail con la potencia de Amazon RDS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La evolución es posible y necesaria,&lt;/strong&gt; trazando un camino claro desde el monolito hacia arquitecturas desacopladas con S3, Lambda y eventualmente EC2, sin necesidad de reconstruir desde cero.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Empresas nacidas en Perú como &lt;strong&gt;Crehana&lt;/strong&gt; o &lt;strong&gt;Chazki&lt;/strong&gt;, y casos de éxito locales en el sur como la transformación digital de &lt;strong&gt;Caja Arequipa&lt;/strong&gt;, demuestran que el talento local, potenciado por la tecnología correcta, no tiene límites.   &lt;/p&gt;

&lt;p&gt;Es hora de que los emprendedores arequipeños dejen de mirar sus servidores locales con preocupación y empiecen a mirar hacia la nube con ambición. Las herramientas están sobre la mesa; la estrategia ha sido trazada. El siguiente unicornio tecnológico puede, y debe, nacer a la sombra del Misti.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;¡Manos a la obra!&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>arequipa</category>
      <category>peru</category>
      <category>emprender</category>
    </item>
  </channel>
</rss>
