Citibank Compliance DQ & Audit Automation
Portfolio project plan, built to mirror what a friend actually does at Citibank: a Data Quality / Compliance Testing role in the Deposits domain — Airflow-orchestrated DQ jobs, rules provided per domain (Loans, Mortgages, Deposits, Credit, Regions), data sourced from an internal data lake via a tool called Genesis. See Profile for career context on why this is on the roadmap.
Three-phase build, each phase shippable on its own, phase 3 depending on phases 1–2's output:
- Deposits DQ Pipeline — domain-specific rules engine + Airflow orchestration
- Credit DQ Pipeline — same generic engine, second domain's rule config (proves reusability)
- Audit Copilot — dashboard (SLI/SLO/SLA) + chat interface over both pipelines' results, the "observability for compliance" layer
Why This Project
Quarterly audits currently take an auditor 3–4 weeks, mostly spent manually gathering evidence: pulling DQ job results, checking rule coverage, cross-referencing lineage. That evidence-gathering step is exactly what's automatable — continuous testing + a queryable result store + a lineage trail turns "re-run everything by hand once a quarter" into "read the dashboard, drill into any exception." What stays human: judgment on ambiguous rules, regulatory attestation/sign-off, and root-cause investigation on genuinely novel failures. The goal of this project is to build the automatable 80% convincingly, not to claim 100%.
Architecture (all 3 phases)
Data Lake (synthetic)
│
▼
Genesis-equivalent ingestion (simple extract step)
│
▼
┌─────────────────────────────┐
│ Airflow DAGs (per domain) │
│ extract → run_dq_rules → │
│ generate_report → alert │
└─────────────────────────────┘
│ │
▼ ▼
Rules Engine Result Store (Postgres)
(generic, reads - rule, domain, region
YAML per domain: - pass/fail counts
deposits.yaml, - failing record IDs
credit.yaml) - source_version (lineage)
- timestamp
│
▼
┌───────────────────────┐
│ Audit Copilot │
│ - SLI/SLO/SLA layer │
│ - Dashboard (trends) │
│ - Chat (RAG over │
│ result store + │
│ rule definitions) │
│ - Audit export (PDF) │
└───────────────────────┘
Key design principle carried through all 3 phases: one generic rules engine, N per-domain YAML configs. This is the real lesson from how production DQ platforms are actually built — Phase 2 should not fork Phase 1's code, it should just add credit.yaml and prove the engine holds up.
Phase 1 — Deposits DQ Pipeline
Goal: stand up the full mechanical loop — synthetic data → declarative rules → Airflow orchestration → report — for one domain.
Tasks
- Generate synthetic deposits dataset (
account_id, customer_id, balance, interest_rate, account_type, region, open_date), seeded with bad records (negative balances, nulls, mismatched regions, duplicate account IDs) - Write
deposits.yaml— declarative rules: not-null, range, referential integrity (customer_id exists), regex format, cross-field logic (savings → interest_rate > 0) - Build the rules engine (pandas or Great Expectations) that reads YAML, applies checks, outputs pass/fail per record + summary
- Stand up local Airflow (Docker Compose) with DAG:
extract_deposits_data → run_dq_rules → generate_report → alert_on_failures - Partition results by
region, simulating region-specific rule thresholds - Persist run output (not just print) — dated, versioned CSV/report: rule ID, description, pass/fail count, sample failing records, timestamp
- Stretch: reconciliation check — aggregate balances between two simulated "systems" (lake vs. downstream)
Deliverable
A working Airflow DAG that runs Deposits DQ checks on demand and produces an auditable report.
Phase 2 — Credit DQ Pipeline
Goal: prove the Phase 1 engine is domain-agnostic by plugging in a second, structurally different rule set — reuse the engine, don't rebuild it.
Tasks
- Generate synthetic credit dataset (
account_id, customer_id, credit_limit, current_balance, available_credit, apr, delinquency_days, payment_due_date, last_payment_date, account_status, region, risk_score), seeded with bad records (balance > limit, negative available_credit, inconsistent delinquency/status, implausible APR) - Write
credit.yaml— reuse referential/not-null patterns from deposits, add credit-specific logic:current_balance <= credit_limitavailable_credit == credit_limit - current_balance- state-machine:
delinquency_days > 0⟹account_status != "current" - region-parameterized APR/usury caps
- freshness rule:
risk_scorerecalculated within N days
- Plug
credit.yamlinto the same engine and DAG shape from Phase 1 — no forked codebase - Cross-domain reconciliation: sum of
current_balanceper customer vs. a "customer risk profile" aggregate exposure field
Deliverable
credit.yaml running through the Phase 1 engine, proving "rules provided by different domains" plugs into one framework — the actual shape of a real DQ platform team's work.
Phase 3 — Audit Copilot (the automation-of-the-audit layer)
Goal: turn Phase 1 + 2's accumulated run history into something an auditor can query in minutes instead of weeks — dashboard for SLI/SLO/SLA, chat interface for ad-hoc "why did X fail" questions, with a full evidence trail.
Scope note: this automates evidence gathering and continuous testing, not the auditor's judgment or regulatory attestation — see "Why This Project" above. Frame all outputs as auditor-assist, not auditor-replacement.
3a. Result Store (persistent, queryable)
- Postgres schema:
dq_runs(run_id, domain, region, rule_id, timestamp, pass_count, fail_count, source_version, dag_run_id) -
dq_failures(run_id, record_id, rule_id, failure_detail)— the actual evidence, not just counts -
rules(rule_id, domain, description, definition_yaml, version, effective_date)— versioned, so a report can cite exactly which rule version ran
3b. Lineage
- Tag every run with
source_version(hash or version string of the synthetic data snapshot it consumed) — minimal stand-in for real lineage - Look at OpenLineage as the real-world standard to reference/imitate
3c. SLI/SLO/SLA layer
- Define SLI examples:
pass_rate per domain/region,pipeline freshness,rule coverage per domain - Define SLO config per domain, e.g.
deposits: pass_rate >= 99.5%,credit: freshness <= 24h - Compute breach status per period, store as a derived table
3d. Dashboard
- Build with Streamlit (fast) or FastAPI + React (more realistic) — SLO status per domain/region over time, trend charts, drill-down into failing records
- This view is what replaces "quarterly audit" with "live status"
3e. Chat interface (RAG over structured audit data, not free-text RAG)
- LLM-backed query layer: user asks e.g. "why did credit domain fail SLO in Region=US in Q2"
- Retrieval step queries the result store + rule definitions (structured SQL/lookup, not vector search over prose) for the relevant runs, rule text, and failing samples
- Response composes an answer with the trail as proof — rule definition → data snapshot version → job run → result → any remediation
- This is the direct analog of "why is Kafka traffic high" observability chat, applied to DQ/compliance data instead of metrics/traces
3f. Audit export
- Generate a point-in-time PDF/report bundling SLO status + evidence trail + rule versions for a given quarter — the artifact an actual auditor would review/sign off on
Deliverable
A dashboard + chat interface sitting on top of Phase 1/2's result history, demonstrating the full "3-4 week audit → minutes of review" narrative with real (if synthetic) evidence trails.
Tech Stack Summary
| Layer | Choice | Why |
|---|---|---|
| Orchestration | Airflow (Docker Compose) | matches real-world tool used in the role |
| Rules engine | Great Expectations (or pandas-based custom) | industry-standard DQ library |
| Rule definitions | YAML, versioned | declarative, auditable, domain-agnostic |
| Result store | Postgres | queryable audit log + lineage |
| Lineage reference | OpenLineage (concept, minimal impl) | real-world standard to speak to |
| Dashboard | Streamlit or FastAPI+React | SLI/SLO/SLA visualization |
| Chat interface | LLM + structured retrieval over Postgres | RAG-over-structured-data, not prose RAG |
| Real-world commercial analogs | Monte Carlo, Bigeye, Soda Cloud, Databand | reference points for scope/credibility |
Open Questions / Decisions To Revisit
- Great Expectations vs. hand-rolled pandas rules engine — GE is more "real," pandas is faster to iterate on early
- Streamlit (fast, less "real") vs. FastAPI+React (slower, more portfolio-credible) for the dashboard
- How much of the chat interface's retrieval should be pure SQL templating vs. actual LLM-driven query generation — start with SQL templating, consider LLM query generation as stretch goal