Module 8: Capstone Project — Production-Ready AI System
8. Summary and Guide Wrap-Up
Description
You reached the end. You completed 8 modules and 3 phases of production practices for AI apps. This wrap-up isn't a summary of concepts — it's a reflection on what you built, why it matters, and what you can do with all of this from now on. Take a moment to look back and recognize how far you've come. Each module you worked through left you a concrete skill you can use tomorrow in a real project — and in this capsule you'll consolidate that complete vision of what it means to take an AI system to production.
The journey: from "works on my laptop" to "works in production"
Week 1 (before the guide):
Your AI app:
- Works when you run it
- Fails mysteriously if the API gives an error
- You don't know how much it costs per request
- You can't test without calling OpenAI
- The prompts are hardcoded in the code
- If there's a bug at 3am, you don't know how to diagnose it
Week N (after the guide):
Your AI app:
- Tests that pass without calling OpenAI (MockProvider)
- If the API fails: retry → circuit breaker → fallback → static default
- Each request logs: cost, latency, request_id
- The prompts are in versioned YAML files
- The domain doesn't know which LLM calls it (clean architecture)
- If there's a bug at 3am: the runbook says exactly what to do
What you built, module by module
Phase 1: Testing (M1-M3)
# Before:
def test_analyze():
result = analyze("This is great!") # ← Calls real OpenAI, costs money
assert result["sentiment"] == "positive" # ← Sometimes fails due to non-determinism
# After:
def test_analyze_with_mock(mock_settings):
provider = MockProvider('{"sentiment": "positive", "score": 0.9, "confidence": 0.8}')
result = analyze_sentiment("This is great!", provider)
assert result["sentiment"] == "positive" # ← Deterministic, free, fast
What remained: a complete test suite — unit tests with mocks, integration tests with semantic assertions, property-based tests for edge cases, and a conftest.py that manages the global fixtures.
Phase 2: Safety & Quality (M4-M6)
# M4: A guardrail that blocks before spending money on the LLM
check = guardrails.check_input(user_text)
if not check.passed:
raise HTTPException(400, check.reason) # ← Doesn't call the LLM, cost $0
# M5: Logging that lets you debug from the logs
log.info("llm_call_completed",
cost_usd=0.0023,
input_tokens=450,
output_tokens=120,
duration_ms=1840,
request_id=get_request_id() # ← Connects all the logs of a request
)
# M6: Domain that doesn't know OpenAI exists
def analyze_sentiment(text: str, provider: LLMProvider) -> dict:
template = load_prompt("sentiment") # ← Prompt in a file, not hardcoded
messages = template.render(text=text)
raw = provider.complete(messages) # ← Injected provider, not instantiated here
return parse_sentiment_output(raw)
What remained: a guardrails pipeline for input/output, structured logging with tracing and cost tracking, and a 4-layer clean architecture where the domain doesn't depend on any concrete provider.
Phase 3: Production (M7-M8)
# M7: The complete reliability layer — a single change in dependencies.py
def build_llm_provider() -> LLMProvider:
base = OpenAIProvider(client, model="gpt-4o", ...)
with_retry = RetryProvider(base, max_attempts=4)
with_cb = CircuitBreakerProvider(with_retry, failure_threshold=5)
rate_limited = RateLimitedProvider(with_cb, rpm=480)
return FallbackProvider([rate_limited, backup_provider],
static_fallback='{"sentiment": "unknown", ...}')
# The domain never changed. ↑ This is the only difference.
# M8: The complete system with checklist, baselines, and runbook
python scripts/run_checklist.py → All items green
python scripts/pre_launch_validation.py → "ALL VALIDATIONS PASSED"
python scripts/benchmark.py → p50: 1.8s, cost: $0.002, errors: 0%
cat docs/RUNBOOK.md → 6 incidents documented with commands
What remained: a production-ready system with all the components integrated, documented, verifiable, and with the scripts to maintain it.
The 5 skills you now have
Skill 1: Testing without the LLM
BEFORE: "I can't test because it always calls OpenAI"
NOW: MockProvider implements the same Protocol
→ Fast, free, deterministic tests
→ The domain tests pass without internet
Skill 2: Making the system visible
BEFORE: "Something failed but I don't know where"
NOW: Each request has a request_id
→ A log entry has: request_id, cost_usd, duration_ms, model, tokens
→ jq 'select(.request_id == "abc123")' logs/app.json → complete history
Skill 3: Protecting the system from malicious inputs
BEFORE: User input goes straight to the LLM → vulnerable to injection
NOW: GuardrailsPipeline.check_input() before each call
→ Injection detected = 400, no LLM cost
→ PII redacted in the output automatically
Skill 4: Surviving LLM failures
BEFORE: OpenAI returns 429 → your app fails with 500
NOW: 429 → RetryProvider waits and retries
30-min outage → CircuitBreaker opens, FallbackProvider serves from secondary
All fail → static_fallback, the user gets a generic response but not error 500
Skill 5: Integrating components
BEFORE: "I have parts working on their own, I don't know how to connect them"
NOW: DI enables composition: FallbackProvider(RateLimited(CircuitBreaker(Retry(OpenAI))))
The domain doesn't change when you add reliability
One file (dependencies.py) controls the whole composition
The production checklist as a permanent tool
The production checklist you saw in capsule 01 isn't for this guide — it's for all your future AI projects:
# Before EACH deploy to production, run:
python scripts/run_checklist.py
# Before the FIRST deploy:
python scripts/pre_launch_validation.py
python scripts/benchmark.py
# If there's an incident:
cat docs/RUNBOOK.md # → Find the incident, follow the steps
When you build a new AI project, take this structure, copy the scripts, adapt the prompts and the domain — the rest (testing, guardrails, logging, reliability) is already built and works.
What comes next: the AI Engineering Path
This guide is one of several in the AI Engineering Path. The practices you learned here are prerequisites for the following ones:
| Guide | What it uses from this guide |
|---|---|
| Monitoring & Observability | The structured logging from M5 as a base |
| Building AI Agents | The clean architecture from M6 + reliability from M7 |
| RAG Systems | The guardrails from M4 + testing patterns from M2-M3 |
| CI/CD for AI Systems | The pre-launch validation script from M8 |
Three things you can do this week
1. Apply one practice to your current project
If you have an existing AI project, choose the practice with the biggest immediate impact:
- No tests → Add MockProvider and write domain tests
- No structured logging → Add structlog with request_id
- No retry → Add RetryProvider with tenacity
You don't have to implement everything at once. One real practice is more valuable than eight practices in an exercise project.
2. Present the Production AI System in your portfolio
The system you built in this guide demonstrates:
- That you know how to test AI without real calls
- That you know the security risks (guardrails)
- That you understand operations (logging, runbook)
- That you can build resilient systems (reliability)
A portfolio entry based on this stands out against projects that "just make the LLM call".
3. Run the checklist on an existing project
Take scripts/run_checklist.py, adapt the paths to your project, and run it. Seeing which items fail will give you a list of concrete, prioritized improvements.
Your growth checklist as an AI Engineer
You've covered the fundamental production practices. But AI engineering evolves fast. Here's a map of what to dig deeper into depending on where you want to grow:
IF YOU WANT TO DIG DEEPER INTO... THEN STUDY...
─────────────────────────────────────────────────────────────────
More sophisticated testing → Property-based testing with Hypothesis
Mutation testing, input fuzzing
LLM evaluations (RAGAS, DeepEval)
Observability in production → OpenTelemetry (distributed traces)
Grafana + Prometheus for metrics
Log aggregation (ELK, Loki, Datadog)
Advanced security → OWASP LLM Top 10 (in depth)
Prompt red teaming
Guardrails with ML classifiers
Architecture to scale → Microservices vs monolith
Message queues (RabbitMQ, SQS)
LLM response caching (Redis)
Reliability at scale → Chaos engineering (Chaos Monkey)
Multi-region deployments
Canary deployments and feature flags
CI/CD for AI → GitHub Actions for ML pipelines
Automated prompt regression testing
Blue/green deployments
Don't try to cover everything at once. Choose a direction based on your current work and dig deep there for 2-4 weeks before moving to another.
Your concrete skills after this guide
Each module left you a skill you can apply tomorrow. Here's the complete inventory of what you now know how to do — not in theory, but with working code:
SKILL WHERE YOU LEARNED IT WHAT YOU CAN DO WITH IT
────────────────────────── ────────────────────── ────────────────────────────────
Create MockProviders M1-M2 Test any AI app without
spending money or depending on APIs
Write semantic assertions M2-M3 Tests that verify LLM behavior,
not exact strings
Design input guardrails M4 Block prompt injection and
prohibited content before the LLM
Redact PII in outputs M4 Comply with privacy regulations
without changing the business logic
Configure structured logging M5 Diagnose production problems
with JSON queries (jq, grep)
Implement request tracing M5 Connect all the logs of a request
with a single request_id
Separate domain from infra M6 Switch providers (OpenAI →
Anthropic) without touching the business
Compose reliability layers M7 Retry → CB → Rate Limit → Fallback
in a single file (dependencies.py)
Create production checklists M8 Verify that your system is ready
before each deploy
Establish performance baselines M8 Measure latency, cost, and error rate
with a reproducible script
Write operational runbooks M8 Resolve incidents at 3am
following documented steps
These aren't isolated skills — they reinforce each other. Structured logging makes the runbook work. MockProviders make the tests fast. Clean architecture makes the reliability layer possible without touching the domain.
How these skills translate to your career
If you're looking for a job or want to advance in your current role, these practices position you at a level that few people in AI engineering have:
IN A TECHNICAL INTERVIEW:
"How do you test your AI app?"
→ "MockProvider that implements the same Protocol. Deterministic tests,
no API key, no cost. Coverage > 70% in domain and processing."
"What happens if OpenAI goes down?"
→ "Retry with exponential backoff for transient errors. If they persist,
circuit breaker opens and the fallback serves from a secondary model or
a static response. The user never sees a 500."
"How do you diagnose a problem in production?"
→ "Structured logging with request_id. A jq query gives me the complete
timeline of any request. The runbook has the commands for
each type of incident."
IN A CODE REVIEW:
- Your code has clear layer separation
- The tests don't depend on external services
- The configuration is in pydantic-settings, not hardcoded
- The prompts are versioned in YAML
What sets you apart now
After completing this guide, you have something most AI developers don't: an integrated understanding of what it takes to bring an AI system to production. You don't just know how to make LLM calls — you know how to build the system that surrounds them.
TYPICAL AI DEVELOPER: YOU AFTER THIS GUIDE:
──────────────────────────── ──────────────────────────────────
"I make requests to OpenAI" → "I have a reliability layer that
survives outages with retry,
circuit breaker, and fallback"
"I add print() when → "I have structured logging with
something fails" request_id, cost tracking,
and queries with jq"
"The tests... are complicated → "My tests run without internet,
with AI" without an API key, and without money
(MockProvider)"
"The prompt is in the code" → "The prompts are in versioned YAML,
with a loader and support for
A/B testing"
"If it goes down, I restart" → "If it goes down at 3am, the runbook
says exactly what to do"
This isn't theory — it's exactly the gap between a junior AI developer and one who can lead a project in production.
Exercises
Exercise 1: Practices self-assessment
For each of the guide's 8 areas, rate your current level from 1 to 5 (1 = I know it in theory, 5 = I can implement it with confidence in a real project). Be honest — the goal isn't to have everything at 5, but to know where to focus your next learning.
| Area | Level (1-5) | Next step if < 4 |
|---|---|---|
| Unit testing with mocks | ___ | Redo the M2 exercises without looking at the guide |
| Integration testing | ___ | Write E2E tests for a personal project |
| Guardrails (input/output) | ___ | Implement PII redaction in a real project |
| Structured logging | ___ | Configure structlog in an existing project |
| Clean architecture + DI | ___ | Refactor a script into domain/infra layers |
| Reliability (retry, CB, fallback) | ___ | Implement a circuit breaker from scratch |
| Production checklist | ___ | Run the checklist on an existing project |
| Runbook + baselines | ___ | Create a RUNBOOK.md for your current project |
See solution
There's no "correct answer" here, but there is an interpretation guide:
If you have 3+ areas at level 1-2: Repeat the corresponding modules, but this time implement in your own project instead of the guide's example. Repetition with a different context is what consolidates learning.
If you have everything at 3: A good starting point. Your next step is to implement each practice in a real project (not an exercise one). The difference between 3 and 4 is having done it in a context with real constraints.
If you have most at 4-5: You're ready for advanced topics: OpenTelemetry, chaos engineering, automated LLM quality evaluations, or CI/CD pipelines for AI.
Pattern to level up:
- Choose your weakest area
- Implement that practice in a real project
- Find an edge case the guide didn't cover
- Solve it — that's what raises your level from 3 to 5
Exercise 2: Apply one practice to an existing project
Choose an AI project you have (or create a small one — it can be a 50-line chatbot with OpenAI). Apply exactly one practice from this guide, the one you consider most impactful for that project.
Document:
- Which practice you chose and why
- How long it took you to implement it
- What difficulty you found that the guide didn't cover
- The before/after in one sentence
See solution
Real example of applying "structured logging with structlog":
Practice chosen: Structured logging (M5)
Why: I had a chatbot in production that failed intermittently and I couldn't diagnose why — I only had print() statements.
Implementation time: 45 minutes
- 10 min: install structlog and configure JSONRenderer
- 15 min: replace print() with log.info()/log.error() with context
- 10 min: add request_id with contextvars
- 10 min: add cost_usd and duration_ms to each LLM call
Difficulty not covered: My app used Flask instead of FastAPI, so the request tracing middleware was different. I had to use Flask's before_request and after_request instead of the Starlette middleware.
Before/after:
- Before: "The chatbot fails sometimes and I don't know why"
- After:
jq 'select(.level == "error")' logs/app.json→ I found that 3% of the requests failed due to an OpenAI timeout, and I added retry in 20 minutes
The practice with the biggest immediate impact is usually different for each project:
- If you have no tests → MockProvider + unit tests
- If you can't diagnose → structured logging
- If your app fails with OpenAI errors → retry + fallback
- If you receive input from users → guardrails
The rule: implement the one that solves your current pain, not the one that sounds most interesting.
Exercise 3: Practices map by module
Create a table that maps each of the 8 modules with: (a) the main concept, (b) the key file or component you implemented, and (c) the command that verifies it works. This serves as a quick reference when you want to apply a practice in another project.
See solution
| Module | Main concept | Key file | Verification command |
|--------|--------------------|---------------|------------------------|
| M1 | Testing fundamentals | `tests/conftest.py` | `pytest tests/unit/ -v` |
| M2 | Unit tests with mocks | `tests/unit/test_sentiment_service.py` | `pytest tests/unit/ -v --tb=short` |
| M3 | Integration testing | `tests/integration/test_api_e2e.py` | `APP_URL=http://localhost:8000 pytest tests/integration/ -v` |
| M4 | Guardrails pipeline | `src/guardrails/pipeline.py` | `pytest tests/unit/test_guardrails.py -v` |
| M5 | Structured logging | `src/logging_config.py`, `src/middleware.py` | `tail -5 logs/app.json \| jq .` |
| M6 | Clean architecture + DI | `src/domain/sentiment_service.py`, `src/app/dependencies.py` | `pytest tests/unit/test_sentiment_service.py -v` |
| M7 | Reliability layer | `src/infrastructure/retry_provider.py`, `circuit_breaker.py` | `pytest tests/unit/test_reliability_integration.py -v` |
| M8 | Production readiness | `scripts/run_checklist.py`, `docs/RUNBOOK.md` | `python scripts/run_checklist.py` |
This map is your personal "cheat sheet". When you start a new project and want to add, for example, structured logging, you come to this table, see that the key file is logging_config.py + middleware.py, and you know exactly what to copy and adapt. The verification column lets you confirm it works in less than 30 seconds.
Exercise 4: Letter to your future self
Write a short document (max 1 page) addressed to yourself 6 months from now, when you're starting a new AI project. Include:
- The 3 practices that had the most impact on your learning
- The 2 errors that were hardest to resolve
- The recommended order to implement the practices in a new project
- A link or reference to this project as a template
See solution
# Notes for my future self — Production Best Practices
## The 3 practices with the most impact
1. **MockProvider + unit tests (M2)**: Being able to test without an API key or cost changed my
development speed. Every time I make a change, I run the tests in 2 seconds.
2. **Structured logging with request_id (M5)**: Before this, debugging was impossible.
Now a `jq 'select(.request_id == "X")'` gives me all the context of the error.
3. **DI with Protocol (M6)**: Separating the domain from the infrastructure made adding
retry, circuit breaker, and fallback trivial — 0 changes in the business logic.
## The 2 errors that were hardest for me
1. **CircuitBreaker wasn't a singleton**: A new one was created on each request and it never
accumulated enough failures to open. The solution: move it to module-level.
2. **Tests that depended on execution order**: A test passed only if it ran
after another that left state. The solution: fixtures with the right scope and teardown.
## Recommended order for a new project
1. Directory structure (domain / infrastructure / app)
2. MockProvider + first unit tests
3. Structured logging (structlog + request_id)
4. Guardrails (input validation before the LLM)
5. Reliability (retry → circuit breaker → fallback)
6. Production scripts (checklist, benchmark)
7. Documentation (README, ARCHITECTURE, RUNBOOK)
## Template
Use `production-ai-system/` as a base. Copy the structure, adapt
the domain and the prompts, keep everything else.
The value of this exercise isn't in what you write today — it's in what you read 6 months from now. The details that seem obvious today are forgotten fast. The decisions you made and the errors you committed are exactly what your future self needs to remember to avoid repeating them.
Troubleshooting
Problem: "I finished the guide but I don't know where to start applying it"
Symptoms:
- You feel you understood everything but you don't know how to start on your own project
- The number of practices feels overwhelming
Solution: Don't try to apply everything at once. Use this prioritization:
PRIORITY 1 (this week): The practice that solves your current pain
- Does your app fail and you don't know why? → Structured logging
- No tests? → MockProvider + unit tests
- Does the LLM fail and your app dies? → RetryProvider
PRIORITY 2 (next 2 weeks): The second most impactful practice
- Already have logs → add guardrails
- Already have tests → add clean architecture
- Already have retry → add circuit breaker + fallback
PRIORITY 3 (next month): The rest
- Production checklist, baselines, runbook
Problem: "My project is very different from the guide's example"
Symptoms:
- Your app isn't sentiment analysis
- You use another framework (Flask, Django) or provider (Anthropic, Gemini)
- Your project's structure doesn't look like the example
Solution: The practices are portable — the sentiment example is just the vehicle:
PRACTICE HOW IT ADAPTS
────────────────────── ─────────────────────────────────
MockProvider Any app that calls an LLM:
create a mock that implements the same
interface (Protocol)
Structured logging Works the same in Flask, Django, or scripts.
Only the request tracing middleware changes.
Guardrails Any app that receives user input
and passes it to an LLM. The injection
patterns are the same.
Clean architecture + DI Applies to any Python project:
separate domain from infrastructure.
Reliability layer Works with any API (Anthropic,
Gemini, custom APIs). Only the provider
at the center changes.
Problem: "I implemented a practice but broke existing tests"
Symptoms:
- You added structured logging and now the tests fail because they expect
print()output - You added DI and the tests don't pass the provider correctly
- You added guardrails and now tests with certain inputs are rejected
Solution: When refactoring, follow this order so you don't break anything:
1. Write the tests for the NEW behavior first
2. Implement the change
3. Verify that the new tests pass
4. Update the old tests that fail
(don't delete them — understand WHY they fail first)
# Example: when adding DI, an old test fails because it called
# the function directly without passing a provider:
# Old test (fails after DI):
result = analyze("test") # ← Doesn't pass a provider
# Updated test:
provider = MockProvider('{"sentiment": "positive", ...}')
result = analyze("test", provider) # ← Passes the mock
The rule: if a refactor breaks more than 3 tests, you're probably making too big a change. Split the refactor into smaller steps.
The final message
Building an AI app that works on your laptop is the first step. Taking it to production with real users is a different problem — one of engineering, not of AI.
The field of AI Engineering exists because the difference between a model that works and a system that serves real users is everything you've learned in these 8 modules: testing that gives confidence, guardrails that protect, logging that lets you understand, architecture that can be maintained, reliability that survives.
It's not theory — it's exactly what teams in production do. The difference between a junior AI engineer and a senior one isn't knowing how to use more models. It's knowing how to build the systems that surround them: systems that are secure, observable, maintainable, and resilient.
Now you know how to do it.
Additional resources
- Google SRE Book — The bible of production systems operations
- Release It! (Michael Nygard) — The reliability patterns book
- OWASP LLM Top 10 — Security in LLM applications
- FastAPI Documentation — The framework's base
- pydantic-settings — Config management
- tenacity Documentation — Retry patterns
- structlog Documentation — Structured logging