This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Read tasks/lessons.md at the start of every session — it records hard-won patterns from past refactors on this codebase.
Car Price Pipeline — ingests ~350k Craigslist vehicle listings via Kafka, processes them through a Medallion Architecture (Bronze → Silver → Gold) on Databricks, and serves insights from a Streamlit dashboard. The serving layer is dbt-duckdb writing to data/warehouse.duckdb.
Orchestrated by Apache Airflow (daily, paused by default). Full pipeline run: ~12–18 min.
# Install dependencies
uv sync --extra dev
# Lint
uv run ruff check .
# Type check
uv run mypy src/ kafka/ dags/ --ignore-missing-imports
# Unit tests (no external services needed — fast)
uv run pytest tests/unit/ -v --tb=short
# Spark tests (requires JVM — slow, ~3s session startup)
uv run pytest tests/ -m spark -v
# Integration tests (requires running Kafka at KAFKA_BOOTSTRAP_SERVERS)
KAFKA_BOOTSTRAP_SERVERS=localhost:9092 uv run pytest tests/integration/ -m integration
# dbt — run from the dbt/ subdirectory
cd dbt && uv run dbt parse --profiles-dir .
cd dbt && uv run dbt compile --profiles-dir .
cd dbt && uv run dbt run --profiles-dir .
cd dbt && uv run dbt test --profiles-dir .
# Start all local infrastructure (Kafka, Airflow, Streamlit, MinIO)
docker compose -f infra/docker-compose.yml up -d
# Airflow UI: http://localhost:8080 (admin/admin)
# Kafka UI: http://localhost:8085
# Dashboard: http://localhost:8501vehicles.csv
│
▼
[Kafka Producer] ──► [Kafka Topic: car-listings] (Avro schema: kafka/schemas/listing.avsc)
│
▼
[Databricks Job — DBR 14.3]
┌──────────────────────────────┐
│ databricks/notebooks/ │
│ 01_bronze_ingest.py │ Kafka → Bronze Delta (DBFS)
│ 02_silver_transform.py │ Clean + filter + deduplicate
│ 03_gold_aggregate.py │ Business aggregations
└──────────────────────────────┘
│
▼
[dbt-duckdb] ──► [data/warehouse.duckdb]
(dbt/models/) │
▼
[Streamlit Dashboard]
(src/interfaces/dashboard/app.py)
src/
domain/ # Pure logic — no framework imports
entities/ # Listing (frozen dataclass)
value_objects/ # Price, Mileage — validated on construction
constants.py # Single source of truth for all filter bounds (min/max price, year, mileage, EXCLUDED_MAKES)
exceptions.py
application/ # Use cases — depends only on domain interfaces
ports/ # IListingProducer (Protocol) — the DDD anti-corruption boundary
produce_listings.py
stream_to_bronze.py
transform_silver.py
infrastructure/ # All I/O — Kafka, Spark/Delta, MinIO, secrets
kafka/ # ListingProducer (satisfies IListingProducer structurally)
spark/ # get_spark_session() factory — single canonical session config
storage/ # MinIO client
secrets/
interfaces/
cli/ # pipeline_cli.py — Click CLI
dashboard/ # app.py — Streamlit
kafka/ (top-level) — standalone Kafka config + producer used outside the DDD src tree (e.g., scripts).
dags/ — Airflow DAG (car_price_pipeline.py) + custom operators.
| Folder | Materialisation | Purpose |
|---|---|---|
dbt/models/silver/ |
view | Staging from Delta |
dbt/models/staging/ |
view | Intermediate staging models |
dbt/models/marts/ |
table | Business marts |
dbt/models/gold/ |
table | Gold aggregations (mart_price_by_make, _state, _year, listings_summary) |
-
Constants are the single source of truth. Filter bounds (price, year, mileage) live in
src/domain/constants.pyfor Python code and indbt_project.ymlvars:for SQL. Never hardcode these values elsewhere — they diverged silently before the refactor. -
Application layer never imports from infrastructure directly.
ProduceListingstakesIListingProducer(fromsrc/application/ports/), notListingProducer. Preserve this boundary. -
overwriteSchemavsmergeSchema. Delta writes that change schema must use.option("overwriteSchema", "true")withmode("overwrite").mergeSchemais for append-only schema evolution. Using the wrong one causesDELTA_FAILED_TO_MERGE_FIELDSerrors on existing Delta tables. -
Column name:
mileagenotodometer. The raw CSV hasodometer; it is renamed tomileageat bronze ingest. All downstream code (domain, dbt) usesmileage. Never reintroduceodometerdownstream. -
dbt reads from MinIO Delta via
delta_scan(). Requireshttpfs+deltaDuckDB extensions ands3_url_style: pathinprofiles.ymlfor MinIO compatibility. -
get_spark_session()is the only Spark session factory. Located atsrc/infrastructure/spark/session.py. Do not create ad-hoc SparkSession instances elsewhere. -
Dashboard table access must be allow-listed.
src/interfaces/dashboard/app.pyvalidates table names against_ALLOWED_TABLESbefore interpolation. Do not bypass this.
- Add aggregation to
databricks/notebooks/03_gold_aggregate.py - Add staging model in
dbt/models/staging/stg_<name>.sql - Add mart model in
dbt/models/gold/mart_<name>.sqlwith CTE pattern (noLIMITinsideWHERE IN) - Add
.ymlfile withnot_null,unique, andaccepted_valuestests - If a new filter bound is introduced — add it to
src/domain/constants.pyanddbt_project.yml vars:
- dbt staging:
stg_<gold_table_name>.sql - dbt gold/marts:
mart_<business_concept>.sql - Databricks notebooks:
NN_<layer>_<verb>.py - Airflow tasks:
snake_caseverb-noun - pytest markers:
spark(needs JVM),integration(needs external services),slow
Credentials are injected via env vars — see infra/.env.example. Required for non-local runs:
DATABRICKS_HOST,DATABRICKS_TOKEN,DATABRICKS_JOB_IDMINIO_ACCESS_KEY,MINIO_SECRET_KEY,MINIO_ENDPOINTKAFKA_BOOTSTRAP_SERVERSAIRFLOW__CORE__FERNET_KEY,AIRFLOW__WEBSERVER__SECRET_KEY
When running scripts on the host against Docker services, use published ports: MinIO→9000, Kafka→9092.