Study guide: Part 2. Why banking data requires specifications

Lesson 3 of 5 in module «Part 2. Why banking data requires specifications»
You are viewing the lesson without signing in. Sign in to save progress and take tests.

Topic: Part 2. Why Banking Data Requires Specifications

Difficulty level: Medium

Estimated study time: 3-4 hours

Prerequisites: Basic understanding of SQL (SELECT, JOIN, GROUP BY, aggregate functions)

Familiarity with the concept of data marts and layered architecture (staging, intermediate, marts)

Initial understanding of dbt (models, not_null/unique/accepted_values tests, the dbt build command)

General awareness of personally identifiable information (PII) and the principles of banking analytics

Learning objectives: Explain why, in the banking domain, a data error is more dangerous than a syntax error and how this differs from errors in web applications.

List the five risk classes of banking data and formulate a verifiable rule (not a slogan) for each.

Distinguish the five layers of specifications in SDD Data (dbt tests, ODCS, ODPS, model specification, reviewer report) and explain what each layer verifies.

Transform a "bad" mart specification into a "good" one by adding grain, PII policy, freshness SLA, mandatory facts, and an owner.

Formulate acceptance criteria for a mart model so that the work of an agent or developer can be rejected on a formal basis.

Overview: Banking data is a domain where "approximately correct" is especially costly: an error in a customer or payments mart may look like a routine table with green tests, yet carry false meaning for reporting, risk management, or customer service. This is precisely why the banking context is convenient for studying SDD Data (Specification-Driven Development for Data) — an approach in which every promise of a data product must be written explicitly, bounded by concrete rules, and confirmed with evidence. The chapter breaks down five risk classes (PII, payments, consents, credit portfolio, reporting), explains why dbt tests alone are insufficient, and introduces a multi-layer model of specifications: tests catch the form and part of the facts, ODCS fixes the technical contract, ODPS describes the data product and the consumer, the model specification describes grain and acceptance facts, and the reviewer report ties the change to the promises. Using a concrete example of the customer_360 mart, the chapter shows the difference between a "confident but empty" bad specification and a "narrow but verifiable" good one. The main practical skill of the chapter is the habit of asking the question "how will this be proven?" and the ability to turn any statement about data into a command, a SQL query, a manual reviewer step, or a human confirmation.

Key concepts: Sdd data: Specification-Driven Development for Data — an approach in which data product development starts from an explicit specification: what the product promises, who needs it, what its constraints are (grain, PII, freshness), and how these promises will be verified. In SDD Data, an agent or developer must not guess what "correct" means — they receive criteria by which their work can be rejected.

Five risk classes of banking data: In the training bank, five risk classes are distinguished: (1) PII and direct identifiers, (2) payments and risk signals, (3) consents to data access, (4) credit portfolio and delinquency, (5) reporting and reproducibility of calculations. Each class requires not only an SQL solution but also a verifiable rule — the slogan "don't disclose personal data" must turn into a concrete list of forbidden columns in the mart layer and a reviewer check.

Grain granularity: The level of detail of a row in a mart, for example "one row per customer_id." This is a data product promise that cannot be inferred automatically from dbt tests — it must be written in the specification. Changing grain silently breaks all downstream reports, so grain must either be fixed or explicitly changed with confirmation from the owner.

Pii and direct identifiers: Personally Identifiable Information — fields by which a customer can be identified directly or indirectly (email, phone, passport_number, taxpayer ID, etc.). In SDD Data, the PII policy is expressed through concrete prohibitions: for example, "models/marts/ does not contain pii_email, phone, passport_number columns," and is verified by a reviewer before release rather than by general phrases about "compliance."

Freshness data freshness: An SLA on the recency of data in the mart, for example "data is no older than 24 hours." Freshness must be written into the specification as a measurable promise, otherwise the consumer cannot tell whether to trust the mart. In dbt this is supported by source freshness, but the policy itself still lives in the specification.

Contract drift: A situation in which the structure or semantics of a mart change unnoticed by the consumer: adding product_code, changing the risk_flag formula, introducing a new level of NULL. Contract drift is caught only by comparing the current version against a fixed specification (ODCS, ODPS, model specification).

Specification layers in sdd data: A multi-layer defense of the data product: (1) dbt tests — form and part of the facts, (2) ODCS — technical contract (fields, types, owners), (3) ODPS — description of the data product and its consumers, (4) model specification — grain and acceptance facts, (5) reviewer report — link between the change and the promises. Each layer catches what the others cannot see.

Odcs and odps: ODCS (Open Data Contract Standard) — a standard for the technical data contract: a list of fields, types, owners, constraints. ODPS (Open Data Product Standard) — a standard for describing a data product: who the consumer is, what task the product solves, what SLAs and acceptance criteria it has. Together they provide both the technical and product sides of the contract.

Acceptance facts: Concrete verifiable statements about a mart that must hold: "customer_id unique/not_null," "total_balance_rub not_null," "risk_event_count_7d is present," "the list of forbidden direct PII does not appear in the mart layer." This is the bridge between the specification and execution — they can be automated in tests or checked manually.

Green dbt build vs usable data: A successful dbt build run only means that the model graph executed and the declared tests passed. It does not prove that the correct granularity was chosen, that the mart does not mix customers and accounts, or that a training risk_flag is not passed off as a production score. This is the boundary of the tool, not its shortcoming.

Verifiable rule vs slogan: The chapter's main skill: any statement about data must turn into a command, a SQL query, a manual reviewer step, or a human confirmation. The slogan "account for risks" is useless for an agent; the rule "mart_customer_360 does not contain columns from the forbidden PII list, checked by the schema reviewer before release" is useful.

Data product owner: A person or role who accepts changes to a mart's contract: whether they agree with a new field, whether they are willing to bear the consequences of a grain change, who to write to when a freshness alert fires. Without an owner, the specification "hangs in the air" and nothing stops drift.

Practice exercises: Name: Turning a bad specification into a verifiable one

Problem: Given a bad mart specification: "Build a customer mart for analysts. Add useful fields about accounts and operations. Verify data quality." Rewrite it in the format of a good specification (following the customer_360 example): indicate the data product name, consumer, mart name, granularity, PII policy, freshness, mandatory facts. Explain which risks each added item closes.

Solution: Step 1. Data product name and consumer. Replace "for analysts" with a concrete role, for example "customer portfolio analysts and the credit risk department." This sets who has the right to demand changes. Step 2. Mart name. Replace "customer mart" with mart_customer_360 and indicate the layer (models/marts/). Step 3. Granularity. Add "one row per customer_id" — otherwise it is impossible to tell what counts as a duplicate. Step 4. PII. Add "the mart contains no direct identifiers: email, phone, passport_number, taxpayer ID." This closes the risk of personal data leakage. Step 5. Freshness. Add "no older than 24 hours" — otherwise the report may rely on a stale snapshot. Step 6. Mandatory facts. Enumerate: customer_id unique/not_null, total_balance_rub not_null, accounts_count >= 0, the forbidden PII columns check passes. This is what "data quality" turns into — verifiable statements. Step 7. Owner. Add "owner: customer analytics team" — who accepts drift. Each item closes one of the five risk classes and turns a slogan into a criterion by which the work can be rejected.

Complexity: beginner

Name: Formulating verifiable rules for three risks

Problem: Take any mart from your dbt project (or an imaginary mart_customer_transactions). Write three risks for it: (1) grain, (2) PII, (3) contract drift. For each risk, formulate not a slogan but a verifiable rule. If the rule cannot be checked — rewrite it until it becomes a command, SQL, or a manual reviewer step.

Solution: Example for mart_customer_transactions: (1) Grain. Slogan: "one transaction per row." Verifiable rule: "SELECT COUNT() FROM (SELECT transaction_id FROM mart_customer_transactions GROUP BY transaction_id HAVING COUNT() > 1) must return 0; the reviewer checks that transaction_id is included in the model's key list and marked as unique." (2) PII. Slogan: "safe." Verifiable rule: "grep -E 'pii_email|phone|passport_number|card_pan' models/marts/mart_customer_transactions.sql must return an empty result; before release the reviewer runs the list_pii_columns.sql script and confirms that the list is empty." (3) Contract drift. Slogan: "the contract is stable." Verifiable rule: "when schema.yml in models/marts/ changes, the agent must generate a diff_spec.md report listing added/removed columns and place it in the PR; the product owner (customer portfolio analyst) leaves an approve comment before merge."

Complexity: intermediate

Name: Analysis: a green dbt build does not yet prove correctness

Problem: In the mart_customer_360, all dbt tests are green: customer_id unique/not_null, total_balance_rub not_null, accepted_values for risk_flag. However, the source_system field accidentally ended up in the mart, and now part of the rows is aggregated by (customer_id, source_system), so the actual grain became "one row per (customer_id, source_system)," although the specification promises "one row per customer_id." Which tests would have missed this problem and why? What needs to be added to the specification to catch such drift in advance?

Solution: Step 1. Why dbt tests did not fire. unique/not_null tests check individual columns, not combinations. A unique test on customer_id will pass even if the table has two rows with the same customer_id and different source_system values — it will see uniqueness if it groups by this field naively (in fact it will fail, but the point is that the grain is wider than expected). The accepted_values test on risk_flag is unrelated to grain at all. Thus, dbt tests see the form, but not the semantics of grain. Step 2. What should be in the specification. An explicit statement: "granularity = one row per customer_id," "source_system is not part of grain and must not appear in the final mart," "the list of allowed columns is fixed in ODCS." Step 3. What checks this enables. A custom dbt test that counts COUNT(DISTINCT customer_id) and compares it with COUNT(*): if they are not equal, the test fails. A manual reviewer step: reconcile the list of mart columns with the ODCS contract before merge. An alert to the owner: if the source_system column appears in diff_spec.md, an approve from the product owner is required.

Complexity: intermediate

Name: Where to fix the PII policy

Problem: The team is arguing where best to fix the PII policy for the customer mart: (a) in the team chat, (b) in a SQL comment at the top of the model, (c) in the specification (ODCS/ODPS/model specification). Choose an option and justify it, relying on the idea of a verifiable rule. Additionally: suggest how to turn the chosen option into an automatic check.

Solution: Step 1. Choosing an option. The correct answer is (c) the specification. Chat (a) is a place where agreements are forgotten within a week and are invisible to new team members, including the agent. An SQL comment (b) is closer, but a comment is documentation without an owner and without automatic verification: it can be accidentally deleted and no one will notice. The specification (c) fixes the policy as an obligation: it has an owner, a version, and is compared against the real schema. Step 2. Automation. (1) ODCS enumerates allowed and forbidden columns. (2) Before release, the list_pii_columns.py script is run, which reads schema.yml in models/marts/ and compares it with the forbidden list — if a match is found, the release is blocked. (3) In the reviewer report (review_report.md) it is recorded: "compared the forbidden PII list, no matches" — this turns a manual step into a reproducible check. Step 3. Connection with the chapter's principle. Any policy must turn into a command, SQL, a manual reviewer step, or a human confirmation. Chat provides none of these four.

Complexity: intermediate

Name: Chapter review questions

Problem: Answer in detail the three review questions from the source: (1) Why does a green dbt build not prove the correctness of a data product? (2) Which defect is more dangerous — a SQL syntax error or a silent change of grain? (3) Where is it best to fix the PII policy — in chat, in an SQL comment, or in the specification?

Solution: (1) dbt build only proves that the model graph executed without errors and that the declared tests passed. It does not verify that the granularity matches the promise, that extra columns did not appear in the mart, that a mix-up of entities (for example, customer and account) did not occur, or that a training risk flag was not passed off as a production score. This is the boundary of the tool: tests catch the form and part of the facts, but not the semantics of the product. (2) A silent change of grain is more dangerous. A SQL syntax error breaks the pipeline — it is visible immediately and blocks the release. A change of grain breaks nothing: tests are green, the report builds, but the numbers become false and that falsehood spreads across all downstream marts and regular reporting. This is precisely why grain is fixed in the specification separately, as the owner's obligation. (3) In the specification. Chat has no version, no owner, and no automatic verification. An SQL comment is better, but is also easily lost and has no owner. The specification (ODCS/ODPS/model specification) is an obligation with an owner, a version, and the ability to automatically reconcile the mart schema with the contract before release.

Complexity: beginner

Case studies: Name: Case 1. Grain drift in the training bank's customer_360 mart

Scenario: A customer portfolio analyst at the training bank asked the data team to add the source_system field to the mart_customer_360 so that it would be possible to track which system a customer came from (mobile bank, branch office, partner program). The agent who received the task added the column to SELECT, updated schema.yml, ran dbt build — all tests are green. The mart's specification had been written earlier in general terms and mentioned neither source_system nor the list of allowed columns.

Challenge: A month later, the credit risk department began complaining that their customer base report was double-counting data: the same customer was being counted twice because now they came from two systems simultaneously. The analyst spent a day figuring out that the mart's grain had effectively changed from "one row per customer_id" to "one row per (customer_id, source_system)," although no one had approved this. There are no syntax errors, dbt tests are still green, but reporting has fallen apart.

Solution: The data team took three actions. (1) Restored the grain by temporarily removing source_system from the mart and leaving it in the intermediate layer models/intermediate/. (2) Clarified the mart_customer_360 specification: explicitly recorded "grain = one row per customer_id," "the list of allowed columns is fixed in ODCS," "any column addition requires a diff_spec.md report and approval from the product owner." (3) Added a custom dbt test that compares COUNT(DISTINCT customer_id) and COUNT(*) on the mart: if they diverge, the test fails. (4) Introduced a mandatory manual reviewer step before merge: reconcile the mart's schema.yml with the ODCS contract and attach the link to the PR.

Result: The mart returned to the promised grain in one release. The custom test and the manual reviewer step would have made such drift impossible in principle, because adding source_system without the owner's explicit decision would have been blocked at the code review stage. The team rated the incident as a "useful strike": it showed that "green dbt build" does not equal "the product can be used," and accelerated the adoption of multi-layer specification across all marts in the project.

Lessons learned: A syntactically correct mart with green tests can be semantically broken — form is not the same as meaning.

Grain is a promise that cannot be inferred from tests; it must be explicitly written in the specification and closed with a separate test on COUNT(DISTINCT).

Adding a "harmless" column to SELECT can silently change grain and break all downstream reporting.

The policy "you can't change the contract without the owner" must be expressed as a mandatory reviewer step, not as a phrase in chat.

Related concepts: Grain_granularity

Contract_drift

Specification_layers_in_SDD_Data

Green_dbt_build_vs_usable_data

Acceptance_facts

Name: Case 2. PII leak through a "safe" transactions mart

Scenario: The data team built mart_customer_transactions — a mart of customer operations for anti-fraud analysts. According to the specification, the mart was supposed to contain only aggregates (sum of operations over 7 days, number of operations, average check) and not contain direct identifiers. The agent generated the model, adding "for debugging" the phone column and the card_pan field in the first 100 rows. dbt tests for not_null passed, the product owner glanced at the PR and merged it.

Challenge: Two weeks later it turned out that the mart was regularly used in a dashboard accessible to analysts from related departments who did not have the right to work with customers' personal data. The phone and card_pan columns were not actually needed by anti-fraud, but ended up in the export and in ad-hoc reports. The legal department requested explanations.

Solution: The team conducted an urgent audit: removed PII columns from the mart, and removed them altogether (they were not needed in the mart layer). Then they introduced four barriers. (1) In ODCS, explicitly listed the forbidden columns: pii_email, phone, passport_number, card_pan, taxpayer ID. (2) Created a pre-commit script list_pii_columns.py that greps models/marts/ against the forbidden list and blocks the commit on a match. (3) Added to the mart_customer_transactions specification the item "PII: the mart contains no direct identifiers; reviewer check is mandatory before release." (4) Introduced a mandatory step in the reviewer report: "compared the forbidden PII list, no matches, date, signature," which is placed in the PR.

Result: The data leak is closed, processes have been tightened. Within the following month, the pre-commit script triggered twice on other models and prevented attempts to add phone and email "for debugging." Anti-fraud analysts noted that the quality of work did not suffer — they really only needed the aggregates. The legal department closed the incident without claims, because the team was able to show that the PII policy is now expressed as verifiable rules rather than slogans.

Lessons learned: "Add for debugging" is a typical path for PII to get into a production mart; debugging must live in the staging/intermediate layers, not in marts.

The slogan "don't disclose personal data" is useless unless turned into a concrete list of forbidden columns and an automatic check.

The PII policy must be fixed in the specification, not in SQL comments and not in chat — otherwise it has no owner and no way to automatically enforce it.

Multi-layer defense (ODCS + pre-commit + manual reviewer step + report) catches what a single barrier misses.

Related concepts: PII_and_direct_identifiers

Verifiable_rule_vs_slogan

ODCS_and_ODPS

Specification_layers_in_SDD_Data

Name: Case 3. Disputed interpretation of risk_flag in the training mart

Scenario: The risk_flag column appeared in mart_customer_360 with values {low, medium, high}. In the source, it is formed as a training signal "the client participated in a training anti-fraud campaign." The mart's specification describes risk_flag as "an aggregated risk indicator," without clarifying that this is a training signal, not a production score. The dbt test accepted_values on {low, medium, high} is green.

Challenge: A quarter later, the credit risk department began using mart_customer_360 as a source for preliminary scoring. They took risk_flag as a ready feature and included it in the limit calculation model. At the quarterly meeting it turned out that "high" in the mart actually meant not "high risk" but "the client was part of the training sample" — that is, the signal is inverted in meaning. The error did not lead to real losses (this is a training bank), but the process was dangerous.

Solution: The team conducted a review. (1) Renamed the column to training_risk_flag, so that the name itself speaks about the nature of the data. (2) Updated ODCS and the specification: added a section "semantics and applicability constraints," where it is explicitly stated that training_risk_flag is a label of belonging to the training sample, not a scoring output, and using it in production models is forbidden. (3) Added a warning in the column description in schema.yml that appears in the generated dbt documentation. (4) Introduced a mandatory step: if someone adds to a mart a column with the word risk or score, the reviewer must attach to the PR a confirmation from the product owner about the nature of the field.

Result: The credit risk department stopped using mart_customer_360 as a source of scoring features and switched to a separate production risk-scoring mart. The semantic confusion is eliminated. The team introduced the practice "any field with a sensitive name (risk, score, limit, status) requires an explicit description of semantics in the specification," which reduced the likelihood of similar errors in the future.

Lessons learned: A column name and an accepted_values test do not protect against false semantics — the value "high" means one thing in one context and another in another.

dbt tests catch the form, but not the interpretation: semantics are the responsibility of the specification.

In SDD Data, not only "which fields exist" matters, but also "how the field MUST NOT be used" — this is also part of the contract.

Dangerous column names (risk, score, limit) require a separate review process — otherwise they are easy to mistake for a finished product.

Related concepts: Contract_drift

Acceptance_facts

Verifiable_rule_vs_slogan

ODCS_and_ODPS

Study tips: Keep a real (or toy) mart from your dbt project at hand: the chapter is easier to understand when you simultaneously rewrite the specification of your own mart, not an abstract customer_360.

Make it a rule: after any paragraph of a specification, ask yourself "how will I verify this?" If there is no answer — rewrite the paragraph until it becomes a command, SQL, a manual reviewer step, or a human confirmation.

Compare "bad" and "good" specifications not by length but by verifiability: a good one looks narrower, because it leaves almost no room for guessing.

Practice turning slogans ("ensure quality," "account for risks") into verifiable rules — this is the chapter's main skill and the main source of errors in production.

Remember that "green dbt build" is a starting point, not a finish line. After it, ask: "is the grain correct? are there extra columns? did I correctly understand the semantics of risk_flag?"

Use the "five risk classes" checklist as a habit: for any new mart, ask whether PII, payments, consents, credit portfolio, and reporting reproducibility are taken into account.

Don't write "comply with banking requirements" in AGENTS.md: for an agent this is an empty phrase. Better enumerate concrete prohibitions, allowed input models, contract fields, and checks.

Start a separate file "Data Risk Notes" with sections PII / Grain / Freshness / Contract drift / Human confirmations — the chapter explicitly asks for such an artifact, and it really helps in daily work.

Learn to write specifications "from the opposite": first imagine exactly how a reviewer or agent will break the mart (add an extra column, change the grain, use a training flag as a production one), and close exactly those paths in the specification.

Distinguish "the table was built" from "the data can be used": the former is a technical fact, the latter is a product obligation. In the banking domain they diverge more often than it seems.

Additional resources: Open data contract standard (odcs): A standard for the technical data contract from the AIDA Data Council. Describes fields, types, owners, constraints. Used in SDD Data as the technical contract layer.

Open data product standard (odps): A standard for describing a data product from the AIDA Data Council. Describes the consumer, the task, the SLA, and acceptance criteria. Used in SDD Data as the product contract layer.

Dbt documentation: tests: A section of the official dbt documentation about tests (generic tests not_null, unique, accepted_values, relationships; custom tests via singular SQL). A source of information about exactly what dbt tests catch.

Dbt documentation: sources and freshness: A documentation section about sources and configuring source freshness. Helps close the data freshness layer in the specification.

Dbt labs blog: "why data tests are not enough": A series of materials from dbt Labs about the limits of automatic data testing. Supports the chapter's idea of multi-layer defense.

Gdpr and 152-fz (russian-language sources): The regulatory basis for working with personal data in the EU and the Russian Federation. Useful as context for understanding why the PII policy in a bank must be expressed as concrete rules rather than slogans.

Book "fundamentals of data engineering" (joe reis, matt housley): A modern textbook on data engineering that covers in detail data contracts, lineage, quality, and reproducibility — the conceptual foundation for the chapter's ideas.

Datafold blog and resources on data contracts: Materials on data contracts and data diffing. Useful for understanding how to automatically catch contract drift between mart versions.

Summary: The main challenge of banking data is not the complexity of SQL, but the fact that an error often looks normal: the table is assembled, tests are green, but the meaning is already broken. That is why, in the banking domain, the discipline of specifications is especially important: any statement about data must turn into a command, SQL, a manual reviewer step, or a human confirmation. The chapter introduces five risk classes (PII, payments, consents, credit portfolio, reporting), five layers of defense (dbt tests, ODCS, ODPS, model specification, reviewer report), and the key skill — distinguishing "the table was built" from "the data can be used." The practical result of the chapter is a filled-in "Data Risk Notes" file with concrete, verifiable rules for PII, grain, freshness, contract drift, and human confirmations. A narrow, verifiable specification is always better than a broad slogan: it is precisely the narrowness that makes the work rejectable, and therefore — manageable.

My notes
0 / 10000

Notes are saved in this browser. They will not appear on another device.

Course menu

Course

SDD Data. Bank Data Platform with Qwen Code and dbt
Progress 0 / 110