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

TermDefinition
Clean ArchitectureOrganizing code in layers with dependencies that point inward
Separation of ConcernsEach module/function has one clear responsibility
God FunctionA function that does too many things and is hard to test and maintain
ProtocolA Python interface that defines a signature without explicit inheritance
Dependency InjectionPassing dependencies as parameters instead of creating them internally
pydantic-settingsLibrary for type-safe configuration management
SecretStrPydantic type that masks the value in repr and logs
lru_cacheDecorator that caches the result of a function
Prompt TemplateConfiguration file with the prompt text and variables
App FactoryFunction that creates and configures the FastAPI app
Fail fastDetect 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 CircuitBreakerProvider wrapper
  • Fallback between models: FallbackProvider already exists, you just have to configure it
  • Budget enforcement: in OpenAIProvider or in a BudgetAwareProvider wrapper
  • 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

  1. Clean Architecture (Robert C. Martin) — The complete conceptual framework
  2. pydantic-settings Docs — Configuration management
  3. typing.Protocol — Interfaces in Python
  4. FastAPI Dependency Injection — Depends() in practice
  5. Refactoring (Fowler) — How to refactor safely
  6. 12-Factor App — The philosophy behind config management and environment management
  7. Domain-Driven Design (Evans) — Domain layer concepts