Module 6: Code Quality Patterns for AI
8. Module 6 Summary and Troubleshooting
Description
This module transformed functional but monolithic code into a maintainable 4-layer architecture. This capsule consolidates all the patterns learned, the most common refactoring errors, and the complete checklist to verify the architecture is correct.
What you built in this module
BEFORE (functional monolithic code):
src/app/main.py (300 lines)
├── Prompts hardcoded as strings in the module
├── from openai import OpenAI directly in the endpoint
├── json.loads() inline mixed with the logic
├── os.getenv() scattered through the code
├── God function: does guardrails + LLM + parse + validate + log
└── Tests require patch("openai.chat.completions.create")
AFTER (4-layer clean architecture):
prompts/sentiment/v1.yaml ← Versionable YAML template
src/config.py ← Type-safe pydantic-settings
src/domain/
└── sentiment_service.py ← 20 lines, orchestration only
src/infrastructure/
├── llm_provider.py ← LLMProvider Protocol
└── openai_provider.py ← Decoupled implementation
src/processing/
└── sentiment_parser.py ← Independently testable parser
src/app/
├── main.py ← App factory
├── dependencies.py ← DI wiring
└── routers/sentiment.py ← Clean endpoint with Depends()
Tests:
test_sentiment_service.py ← MockProvider, without patch()
test_sentiment_parser.py ← Parser in isolation
test_config.py ← Production validations
Decision map: where does each thing go?
Where does this code go?
│
├─ Is it the text/content told to the LLM?
│ └─ PROMPT TEMPLATES: prompts/*.yaml
│ Example: "Analyze the sentiment of: {text}"
│
├─ Is it the business rule? ("minimum confidence is 0.3")
│ └─ DOMAIN: src/domain/
│ Example: analyze_sentiment(), LowConfidenceError
│
├─ Is it a call to an external API (OpenAI, Anthropic)?
│ └─ INFRASTRUCTURE: src/infrastructure/
│ Example: OpenAIProvider.complete(), HTTP request
│
├─ Is it transforming LLM text into a Python type?
│ └─ PROCESSING: src/processing/
│ Example: parse_sentiment_output(), SentimentOutput
│
├─ Is it system config (model, temperature, API key)?
│ └─ CONFIG: src/config.py
│ Example: Settings.model, Settings.temperature
│
└─ Is it the HTTP entry point?
└─ APP: src/app/
Example: FastAPI endpoints, middleware, Depends()
The 6 anti-patterns eliminated and how to detect them
Anti-pattern 1: Prompt hardcoded in code
# ❌ Detect:
grep -r "f\"Analyze" src/
# If it appears in a .py other than loader.py, it's an anti-pattern
# ❌ Problematic code:
PROMPT = f"Analyze sentiment: {text}"
# ✅ Solution:
# prompts/sentiment/v1.yaml
# template: "Analyze sentiment: {text}"
# Code: load_prompt("sentiment/v1").render(text=text)
Anti-pattern 2: OpenAI import in domain
# ❌ Detect:
grep -r "from openai" src/domain/
# If anything appears, it's an anti-pattern
# ❌ Problematic code:
# src/domain/sentiment_service.py
from openai import OpenAI # ← Domain knows OpenAI
# ✅ Solution:
# src/domain/sentiment_service.py
from src.infrastructure.llm_provider import LLMProvider # Only the Protocol
Anti-pattern 3: Config with scattered os.getenv
# ❌ Detect:
grep -r "os.getenv" src/ --include="*.py"
# If it appears outside config.py, it's an anti-pattern
# ❌ Problematic code:
MODEL = os.getenv("MODEL", "gpt-4o-mini") # In models.py
TEMP = float(os.getenv("TEMPERATURE", "0.7")) # In utils.py
# ✅ Solution:
settings = get_settings()
settings.model, settings.temperature
Anti-pattern 4: json.loads() in domain
# ❌ Detect: JSON parsing in domain
# src/domain/sentiment_service.py
data = json.loads(response) # ← Processing concern in domain
# ✅ Solution:
result = parse_sentiment_output(response) # Delegate to the parser
Anti-pattern 5: patch() of OpenAI in unit tests
# ❌ Fragile tests:
@patch("openai.chat.completions.create")
def test_analyze(mock_create):
mock_create.return_value = MagicMock(...) # 10 lines of setup
# ✅ Tests with DI:
def test_analyze():
mock = MockProvider('{"sentiment": "positive", "score": 0.8, "confidence": 0.9}')
result = analyze_sentiment("Great!", mock)
assert result["sentiment"] == "positive" # 3 lines total
Anti-pattern 6: God function without separation
# ❌ Detect: a function with multiple responsibilities
def analyze(text: str) -> dict:
# guardrail: if len(text) > 5000...
# prompt construction: prompt = f"..."
# LLM call: client.chat.completions.create(...)
# JSON parse: json.loads(raw)
# validation: if score > 1.0: score = 1.0
# logging: print(f"Done: {result}")
return result
# ✅ Solution: each responsibility in its layer
# The domain function only orchestrates
def analyze_sentiment(text: str, provider: LLMProvider) -> dict:
messages = [{"role": "system", ...}, {"role": "user", ...}]
raw = provider.complete(messages)
return parse_sentiment_output(raw) # 3 clear lines
The 5 most common refactoring errors
Error 1: Moving code before having tests
Symptom: After moving code, something fails but you don't know where.
Cause: You didn't have tests for the function before refactoring.
Diagnosis:
- Do you have tests that cover the function you're going to move?
- pytest tests/ → how many pass BEFORE the change?
Fix:
1. FIRST: add tests for the current function (in its original state)
2. THEN: move the code
3. Run tests → they must still pass
Error 2: Violating the dependency rule
# Symptom: domain tests require an OpenAI API key
# Cause: domain imports from infrastructure
# src/domain/service.py
from src.infrastructure.openai_provider import OpenAIProvider # ← INCORRECT
# Diagnosis:
grep -r "from src.infrastructure" src/domain/
# Fix: domain should only import the Protocol
from src.infrastructure.llm_provider import LLMProvider # Only the Protocol
def analyze(text: str, provider: LLMProvider) -> dict: ... # DI
Error 3: Settings don't load with the correct values
# Symptom: tests read the project's .env instead of the test config
# Cause: lru_cache stores the config from the first get_settings()
settings_1 = get_settings() # Reads .env
settings_2 = get_settings() # Returns the same cached object
# Fix: clear the cache before each test
@pytest.fixture(autouse=True)
def clear_settings_cache():
get_settings.cache_clear()
yield
get_settings.cache_clear()
Error 4: load_prompt fails in tests because it can't find the file
# Symptom: FileNotFoundError: Prompt not found: .../prompts/sentiment/v1.yaml
# Cause: the relative path in load_prompt() is relative to the working directory,
# which may differ when you run pytest from different directories
# Fix: use Path(__file__) instead of relative paths
PROMPTS_DIR = Path(__file__).parent.parent.parent / "prompts"
# This path is absolute and works from any working directory
# Alternative fix: in pytest.ini
[pytest]
testpaths = tests
rootdir = . # Ensures the root directory is correct
Error 5: FallbackProvider doesn't work because the errors are the wrong type
# Symptom: FallbackProvider doesn't catch the error and doesn't fall back
# Cause: the raised error is not LLMProviderError but the original error
# (openai.RateLimitError, httpx.TimeoutException, etc.)
# Fix in OpenAIProvider: always wrap in LLMProviderError
try:
response = self._client.chat.completions.create(...)
return response.choices[0].message.content
except Exception as e:
raise LLMProviderError(str(e), original_error=e) # ← ALWAYS wrap
Module 6 production checklist
ARCHITECTURE
[ ] src/domain/ doesn't import from src/infrastructure/ (only the Protocol)
[ ] src/domain/ doesn't import from openai, anthropic, or httpx
[ ] src/processing/ doesn't import from src/infrastructure/
[ ] Prompts are in prompts/*.yaml, not hardcoded in .py
CONFIG
[ ] All config goes through get_settings()
[ ] No os.getenv() outside config.py and startup.py
[ ] openai_api_key uses SecretStr
[ ] model_validator rejects use_mock_llm=True in production
[ ] model_validator rejects log_level=DEBUG in production
[ ] .env.example documented with all variables
DEPENDENCY INJECTION
[ ] Endpoints use Depends(get_llm_provider)
[ ] Domain functions receive LLMProvider as an argument
[ ] get_llm_provider() returns MockProvider if use_mock_llm=True
[ ] FallbackProvider available if high availability is needed
TESTS
[ ] Domain unit tests use MockProvider, not patch("openai...")
[ ] Parser unit tests are independent of the provider
[ ] Config tests verify production validations
[ ] pytest tests/ → all pass after the refactoring
QUICK VERIFICATION
[ ] grep -r "from openai" src/domain/ → no results
[ ] grep -r "os.getenv" src/ (except config.py) → no results
[ ] grep -r "f\".*{text}" src/domain/ → no results (prompts in YAML)
[ ] pytest tests/ → all pass
Module vocabulary
| Term | Definition |
|---|---|
| Clean Architecture | Organizing code in layers with dependencies that point inward |
| Separation of Concerns | Each module/function has one clear responsibility |
| God Function | A function that does too many things and is hard to test and maintain |
| Protocol | A Python interface that defines a signature without explicit inheritance |
| Dependency Injection | Passing dependencies as parameters instead of creating them internally |
| pydantic-settings | Library for type-safe configuration management |
| SecretStr | Pydantic type that masks the value in repr and logs |
| lru_cache | Decorator that caches the result of a function |
| Prompt Template | Configuration file with the prompt text and variables |
| App Factory | Function that creates and configures the FastAPI app |
| Fail fast | Detect and report configuration errors at startup, not at runtime |
.env.{environment} | Environment-specific configuration file |
Connection with Module 7
Module 7 (Reliability Patterns & Production Checklist) adds the last technical layer: the system's ability to survive when things fail.
This module's clean architecture makes Module 7 straightforward:
- Retry logic: goes in
OpenAIProvider.complete()— infrastructure, not domain - Circuit breaker: also in infrastructure or in a new
CircuitBreakerProviderwrapper - Fallback between models:
FallbackProvideralready exists, you just have to configure it - Budget enforcement: in
OpenAIProvideror in aBudgetAwareProviderwrapper - Health checks: Module 6's startup checks are the basis for Module 7's health checks
The transition: "Your code is clean and organized → now add the ability to recover when things fail."
Additional module resources
- Clean Architecture (Robert C. Martin) — The complete conceptual framework
- pydantic-settings Docs — Configuration management
- typing.Protocol — Interfaces in Python
- FastAPI Dependency Injection — Depends() in practice
- Refactoring (Fowler) — How to refactor safely
- 12-Factor App — The philosophy behind config management and environment management
- Domain-Driven Design (Evans) — Domain layer concepts