1 Why Data Vault? The Enterprise Integration Challenge

ShaDey Inc. operates a multi-source retail ecosystem: SAP ECC for enterprise resources, Salesforce for CRM, Shopify for eCommerce, NCR for point-of-sale, and dozens of custom APIs feeding inventory, logistics, and financial data. Data Vault 2.0 is a hybrid architecture that solves a fundamental problem: how do you integrate this chaos without forcing a single logical view upfront? Data Vault defers business rules to a consumption layer, capturing raw source-aligned structures in the warehouse so they remain queryable, auditable, and adaptable as requirements evolve.

Data Vault is not a reporting layer (that's the star schema). It is not a source system (that's SAP, Salesforce, etc.). It is a governance layer that captures business keys, relationships, and change over time in a source-aligned, non-destructive format. Build it once, query it a thousand ways.
Scenario Best Fit Why ShaDey Fit
Single source system (e.g., only SAP) Dimensional/Star Schema No integration overhead; direct ETL → dimensional tables No (multi-source)
10+ heterogeneous sources, complex keys Data Vault 2.0 Source-aligned raw layer buffers business rule changes; efficient key governance Yes (SAP, CRM, eCommerce, POS)
Real-time (sub-second) requirements Streaming + OLTP Cache Data Vault adds latency (nightly loads default); use Kafka/Redis for real-time Partial (POS real-time, SAP nightly)
Audit trail mandatory (HIPAA, SOX) Data Vault (Raw Vault tier) All changes captured with load_date, record_source, hashdiff for data integrity Yes (eCommerce compliance)
Small team, simple pipeline, 5GB total data Dimensional + ELT Vault overhead (Hubs, Sats, Links, staging) not justified No (ShaDey: 200M transactions/year)
If you have a single source system with stable requirements, skip Data Vault. Dimensional modeling alone is faster to implement and easier to teach. Data Vault shines only when (1) multiple sources need integration, (2) business rules are volatile, or (3) audit trails are non-negotiable. Forcing Vault on a simple pipeline wastes engineering time.

2 Hubs: Identities, Not Entities

A Hub is the atomic business key container. Not an entity. Not a dimension. A collection of business keys that define an identity anchor in your enterprise. In ShaDey Inc., a "Customer" is not a Hub because "Customer" is an entity with attributes. Instead, HUB_CUSTOMER is the set of all customer business keys ever encountered across all sources: SAP customer numbers, Salesforce account IDs, Shopify customer IDs, loyalty card numbers. The Hub stores only the business key, a hash key (surrogate), load date, and record source. All descriptive attributes (name, address, email) live in Satellites.

Data Vault separates identity (Hub) from description (Satellite). An identity never changes. A customer ID is always that customer ID. But the customer's address, email, segment, and lifetime value change. Capture the unchanging identity in the Hub; capture the changing attributes in Satellites with effective dating.

ShaDey Inc. Hub Inventory

Hub Name Business Key(s) Source Systems Record Count (Est.)
HUB_CUSTOMER customer_bk (multi-part: SAP#|SF#|Shopify#) SAP KNA1, Salesforce Accounts, Shopify API 50M
HUB_PRODUCT product_bk (EAN/SKU) SAP MARA, Shopify products, POS catalog 120K
HUB_STORE store_bk (location code) SAP SFRA, Salesforce territory, Company intranet 850
HUB_VENDOR vendor_bk (supplier ID) SAP EINA, Ariba, Catalog 12K
HUB_ORDER order_bk (order#|system) SAP VBAK, Shopify orders, POS system 200M

Example: HUB_CUSTOMER DDL

SQL: Hub Structure
CREATE TABLE raw_vault.hub_customer ( hub_customer_hk BINARY(16) PRIMARY KEY, -- MD5(customer_bk) customer_bk VARCHAR(255) NOT NULL, -- SAP#|SF#|Shopify#, composite key load_date DATE NOT NULL, -- First load; never updated record_source VARCHAR(50) NOT NULL -- 'SAP_KNA1' | 'SALESFORCE' | 'SHOPIFY' ); CREATE UNIQUE INDEX ix_hub_customer_bk ON raw_vault.hub_customer(customer_bk);
HUB_CUSTOMER HUB_PRODUCT HUB_STORE HUB_VENDOR HUB_ORDER
Hub business keys must be governed. Document the composite key structure. Maintain a business key governance registry so all teams know: is a customer identified by SAP#, or SAP#+Salesforce#, or SAP#+Salesforce#+Shopify#? A messy key strategy cascades through Satellites and Links.
HUB_CUSTOMER customer_bk hash_key, load_date SAT_CUSTOMER DEMOGRAPHICS (SAP) 1:N SAT_CUSTOMER CRM_PROFILE (Salesforce) 1:N SAT_CUSTOMER ECOMMERCE (Shopify) 1:N HUB_PRODUCT EAN/SKU HUB_STORE location_code LINK_ORDER (Link) LINK_SALE (Link) Hub (Business Key) Satellite (Attributes) Link (Relationships)
Hub-Satellite-Link Entity Relationship Model: HUB_CUSTOMER (center) with three satellites capturing source-aligned attributes from SAP, Salesforce, and Shopify. HUB_PRODUCT and HUB_STORE connect via Links (LINK_ORDER, LINK_SALE) for relationship cardinality.

3 Satellites: Tracking Change, Not Storing History

A Satellite attaches descriptive attributes to a Hub, with an effective date and a hashdiff. Unlike slowly changing dimension (SCD) Type 2, which stores old rows until they age out, Satellites retain all versions forever. Every time a customer's email changes, a new row enters SAT_CUSTOMER_CONTACT_INFO with a new load_date and a hashdiff showing that email (and only email) changed. The hashdiff is a hash of all non-key columns, allowing you to detect that "this payload changed" without comparing 47 columns individually.

ShaDey Satellites: Customer Context

Satellite Source Key Attributes Change Frequency
SAT_CUSTOMER_DEMOGRAPHICS SAP KNA1 name, country, region, industry_code, credit_group Monthly
SAT_CUSTOMER_CRM_PROFILE Salesforce account_type, segment, opp_pipeline, lifetime_value Weekly
SAT_CUSTOMER_ECOMMERCE Shopify Events email, last_login, device_preference, cart_abandonment_ct Daily
SAT_CUSTOMER_LOYALTY Loyalty Platform tier, points_balance, join_date, tier_expiry Daily

Example: SAT_CUSTOMER_DEMOGRAPHICS DDL + Sample Data

SQL: Satellite Structure
CREATE TABLE raw_vault.sat_customer_demographics ( hub_customer_hk BINARY(16) NOT NULL, load_date DATE NOT NULL, sat_hashdiff BINARY(16) NOT NULL, -- MD5 of all non-key columns customer_name VARCHAR(255), country_code CHAR(2), region VARCHAR(100), industry_code VARCHAR(10), record_source VARCHAR(50), PRIMARY KEY (hub_customer_hk, load_date) );

Sample Data: Customer 12345 Attribute Changes

hub_customer_hk load_date customer_name country_code sat_hashdiff record_source
0x2a1b... 2024-01-15 Acme Corp Ltd US 0xabc1... SAP_KNA1
0x2a1b... 2024-03-22 Acme Corporation US 0xdef2... SAP_KNA1
0x2a1b... 2024-06-10 Acme Corporation CA 0xfgh3... SAP_KNA1

Interpretation: Customer's name changed on 2024-03-22 (hashdiff changed). Country moved US → CA on 2024-06-10. All three versions remain in the satellite forever. Query SAT_CUSTOMER_DEMOGRAPHICS WHERE load_date = DATEADD(day, -1, GETDATE()) to get the current row; query WHERE load_date BETWEEN @start AND @end for historical context.

A hashdiff (hash of all descriptive columns) detects change without column-by-column comparison. When you load a new SAP extract, you compute the hashdiff on every row. If hashdiff matches the last loaded hashdiff, skip the row (no change). If it differs, insert a new row with the new load_date. This is how Data Vault handles SCD Type 2 efficiently at scale: one hash compare instead of 47 column compares.

5 Raw Vault vs Business Vault: The Layering Strategy

Raw Vault is source-aligned: one Satellite per source system per entity. SAP customer data → SAT_CUSTOMER_SAP_DEMOGRAPHICS. Salesforce customer data → SAT_CUSTOMER_SFDC_PROFILE. Same Hub, different Satellites. Raw Vault is audit-grade: every byte from every source is preserved. Business Vault layers on top: it combines Raw Vault data, applies golden record logic, deduplicates, standardizes, and produces a single "best version" for consumption. Raw Vault is READ-ONLY; Business Vault is the OUTPUT of ETL transformation logic.

Source
SAP KNA1
Source
Salesforce
Source
Shopify
Raw Vault
HUB_CUSTOMER + SATs
Source-aligned, audit-grade, immutable
Business Vault
SAT_CUSTOMER_GOLDEN
Merged, business rules applied, queryable
Consumption
DIM_CUSTOMER
Star schema, optimized for BI

Example: Golden Record for Customer Address

Hub Raw Sat Source Address Status
hub_cust_hk=0x2a1b SAT_CUSTOMER_SAP 123 Main St, Springfield, IL 62701 Ship address
hub_cust_hk=0x2a1b SAT_CUSTOMER_SFDC 123 Main Street, Springfield, Illinois 62701 Billing address
hub_cust_hk=0x2a1b SAT_CUSTOMER_SHOPIFY 123 Main St Springfield IL 62701 eCommerce address

Raw Vault Strategy: Three Satellites, each with the exact address string from its source. No standardization. Audit trail is perfect.

SQL: Business Vault Derivation (Golden Record)
CREATE TABLE biz_vault.sat_customer_golden_address AS SELECT rc.hub_customer_hk, MAX(CASE WHEN rc.record_source = 'SFDC' THEN 3 WHEN rc.record_source = 'SAP' THEN 2 WHEN rc.record_source = 'SHOPIFY' THEN 1 END) priority, FIRST_VALUE(TRIM(rc.address_street)) OVER (PARTITION BY rc.hub_customer_hk ORDER BY priority DESC) golden_street, FIRST_VALUE(UPPER(rc.city)) OVER (PARTITION BY rc.hub_customer_hk ORDER BY priority DESC) golden_city, GETDATE() load_date FROM raw_vault.sat_customer_sap_address rc INNER JOIN raw_vault.sat_customer_sfdc_address USING (hub_customer_hk) WHERE rc.load_date = DATEADD(day, -1, CAST(GETDATE() AS DATE));
Raw Vault is immutable and source-aligned; you can replay it anytime. Business Vault is transformation output; if the golden record logic changes, you re-run Business Vault. Raw Vault answers "what did SAP tell us?"; Business Vault answers "what do we believe to be true?" Both are essential.
RAW VAULT SAT_CUSTOMER_SAP SAT_CUSTOMER_CRM SAT_CUSTOMER_ECOM Source-aligned Audit-grade Immutable All source data preserved exactly BUSINESS VAULT SAT_CUSTOMER_GOLDEN BUS_SAT_ADDRESS Merged data Business rules Golden records Deduplication Standardization STAR SCHEMA DIM_CUSTOMER FACT_SALES BI-ready Star schema Optimized joins Analysis ready One-way ETL Transform Map
Data Vault Layering Strategy: Raw Vault (amber) preserves source-aligned satellites with complete audit trail. Business Vault (blue) merges raw data, applies business rules, and produces golden records. Dimensional consumption layer (green) creates star schema optimized for BI analytics.

6 Vault → Dimensional Mapping: The Consumption Bridge

The star schema (DIM + FACT) remains the gold standard for BI. Data Vault feeds it. HUB_CUSTOMER + SAT_CUSTOMER_GOLDEN + SAT_CUSTOMER_DEMOGRAPHICS → DIM_CUSTOMER. LINK_ORDER_PRODUCT + SAT_LINK_OP_LINEITEM → FACT_SALES. This is a one-way ETL pipeline: Data Vault is the source; star schema is the derived artifact. If the star schema changes (e.g., add a new column), you rebuild it from Vault. The Vault data itself is never modified.

Vault Component → Dimensional Artifact Mapping Logic
HUB_CUSTOMER DIM_CUSTOMER (surrogate key) 1:1; hub_customer_hk → customer_sk
SAT_CUSTOMER_GOLDEN DIM_CUSTOMER (descriptive columns) Latest version per hub_customer_hk; join on hub_customer_hk
SAT_CUSTOMER_DEMOGRAPHICS DIM_CUSTOMER (demographics columns) Join on hub_customer_hk; filter load_date ≤ fact_date
HUB_PRODUCT DIM_PRODUCT (surrogate key) 1:1; hub_product_hk → product_sk
LINK_ORDER_PRODUCT + SAT_LINK_OP_LINEITEM FACT_SALES Link hks + Satellite attributes (qty, price); grain is order-line
HUB_ORDER + SAT_ORDER_PAYMENT FACT_SALES (order-level measures) order_hk → order_sk; payment Sat → amount, payment_method

Example: DIM_CUSTOMER Creation from Data Vault

SQL: Dimensional Transformation
CREATE TABLE star_schema.dim_customer AS SELECT ROW_NUMBER() OVER (ORDER BY h.hub_customer_hk) customer_sk, h.hub_customer_hk, h.customer_bk, g.customer_name, d.country_code, d.region, d.industry_code, c.account_type, c.segment, c.lifetime_value, e.email, e.last_login, h.load_date dw_insert_date, GETDATE() dw_update_date FROM raw_vault.hub_customer h LEFT JOIN biz_vault.sat_customer_golden g ON h.hub_customer_hk = g.hub_customer_hk LEFT JOIN raw_vault.sat_customer_demographics d ON h.hub_customer_hk = d.hub_customer_hk AND d.load_date = DATEADD(day, -1, CAST(GETDATE() AS DATE)) LEFT JOIN raw_vault.sat_customer_crm_profile c ON h.hub_customer_hk = c.hub_customer_hk AND c.load_date = DATEADD(day, -1, CAST(GETDATE() AS DATE)) LEFT JOIN raw_vault.sat_customer_ecommerce e ON h.hub_customer_hk = e.hub_customer_hk AND e.load_date = DATEADD(day, -1, CAST(GETDATE() AS DATE));
DIM_CUSTOMER DIM_PRODUCT FACT_SALES DIM_STORE
Always flow: Raw Sources → Raw Vault (immutable) → Business Vault (transformed) → Star Schema (consumption). Never modify Raw Vault for downstream needs. Never write back to Raw Vault. The pipeline is unidirectional. If star schema requirements change, you re-derive it from Vault, not patch it in place.

7 When NOT to Use Data Vault: The Pragmatic Caveat

Data Vault has overhead: business key governance, Hub/Satellite/Link modeling, hashdiff computation, multiple layers (Raw → Business → Star). For a small team or simple pipeline, that overhead is waste. Data Vault earns its keep when you have (1) multiple heterogeneous sources, (2) volatile business rules, or (3) audit compliance requirements. If none of these apply, skip Data Vault and build a dimensional model directly from sources.

Data Vault adds 30–50% modeling complexity. In exchange, you get immutable audit trails, source-aligned flexibility, and a buffer against changing business requirements. The ROI appears when you have 5+ source systems or when re-modeling cost exceeds initial modeling cost. For a single-source, stable domain: use dimensional modeling and save the effort.

Decision Matrix: Data Vault vs Dimensional

Scenario Data Volume Source Count Requirement Stability Recommendation
Prototype / POC < 100 GB 1–2 Fluid (will change) Dimensional; iterate fast
Stable single-source (e.g., only SAP) 1–10 TB 1 Stable Dimensional star schema; direct ETL
Multi-source, stable rules 1–10 TB 3–5 Stable Hybrid: Dimensional with a staging layer per source
Multi-source, volatile requirements, high audit burden > 10 TB 5+ Volatile or compliance-driven Data Vault 2.0
Real-time streaming (Kafka, Pub/Sub) N/A (unbounded) N/A N/A OLTP cache + dimensional snapshot; Vault for batch integration
Forcing Data Vault on a simple single-source pipeline is a sunk cost fallacy. You will spend 6 months building Hubs, Satellites, and Links when a 2-month dimensional model + ELT would have sufficed. Only adopt Data Vault if the multi-source, volatile-requirement, or audit-compliance benefit materially outweighs the modeling overhead.

8 Model Validation Framework: Business Validation

Before you load a terabyte of data into production, validate that the model answers the business questions. Gather stakeholders. Ask: "Can you derive Customer Lifetime Value from this model? Can you trace a product from supplier to store? Can you identify customers at churn risk?" If the answer is "no" or "with complex SQL," the model is incomplete.

ShaDey Inc. Business Validation Checklist

  • KPI Derivation: Can you compute Customer Lifetime Value from HUB_CUSTOMER + SAT_CUSTOMER_CRM_PROFILE + LINK_ORDER_CUSTOMER + SAT_LINK_OP_LINEITEM? (Order history + payment data)
  • Churn Risk: Can you identify customers with zero purchases in last 90 days? (Requires JOIN: Hub → Link → Order Sat with load_date filter)
  • Supplier Impact Analysis: Can you find all customers affected if Vendor X fails? (HUB_VENDOR → LINK_PRODUCT_VENDOR → LINK_ORDER_PRODUCT → LINK_ORDER_CUSTOMER)
  • Product Substitution: Can you map competitors' products to your products in the model? (Requires a cross-catalog Satellite or Business Vault table; is it present?)
  • Inventory Visibility: Can you see current stock at each store for each product? (Requires HUB_PRODUCT + HUB_STORE + LINK_STORE_PRODUCT + SAT_INVENTORY; is inventory modeled?)
  • Historical Replay: Can you reconstruct Q3 2023 customer segments, orders, and inventory? (All Satellites must include load_date; all joins must be load_date-aware)
  • Source Attribution: Can you report "Product X was sourced from Vendor Y on [date]"? (LINK_PRODUCT_VENDOR must have effective dating or a SAT_LINK_PV_SOURCING)
  • Audit Trail: Can you prove to compliance that "this order was recorded exactly as received"? (Raw Vault must be immutable; record_source must be traceable)
Run a business validation workshop. For each KPI, write the query together. If the query is simple (3 joins, < 10 seconds), the model is good. If the query is complex (10+ joins, > 60 seconds), the model is missing entities or relationships. Missing business logic? Add Satellites. Missing relationships? Add Links. Iterate until stakeholders say "yes, this is correct."

9 Model Validation: Logical Consistency Checks

After business validation, perform logical validation: does the model structure obey referential integrity, cardinality constraints, and naming conventions? Use data quality rules to catch surprises before they propagate.

Logical Validation Checklist

  • Referential Integrity: Every hub_*_hk in a Link exists in the corresponding Hub. Query: SELECT link.hub_order_hk FROM link_order_product link WHERE NOT EXISTS (SELECT 1 FROM hub_order h WHERE h.hub_order_hk = link.hub_order_hk) should return 0 rows.
  • Cardinality Verification: Is the relationship documented (1:1, 1:M, M:N)? Spot-check with COUNT DISTINCT. Example: SELECT COUNT(DISTINCT customer_bk), COUNT(DISTINCT hub_customer_hk) FROM hub_customer; should be equal (no collisions).
  • Attribute Domain Validation: country_code is always ISO 3166 (2-char). status is one of ('ACTIVE', 'INACTIVE'). Validate via SELECT DISTINCT country_code FROM sat_customer_demographics WHERE LENGTH(country_code) != 2; should return 0 rows.
  • Naming Convention Compliance: All Hubs start with hub_. All Satellites start with sat_. All Links start with link_. All surrogate keys end with _hk (hash key) or _sk (surrogate key). All business keys end with _bk.
  • Hashdiff Consistency: Hashdiff must be reproducible. Load the same source row twice; hashdiff must match. If it differs, salt/padding is wrong or column order is non-deterministic.
  • Load Date Monotonicity: load_date must be >= previous load_date for the same business key. Check: SELECT * FROM sat_customer_demographics WHERE hub_customer_hk = @hk ORDER BY load_date DESC; must show descending dates.
  • No NULLs in Keys: hub_*_hk, *_bk, load_date, record_source must be NOT NULL. Spot-check: SELECT COUNT(*) FROM hub_customer WHERE hub_customer_hk IS NULL; should be 0.
Encode these checks as automated data quality tests. Run them nightly. Alert when a rule fails. Examples: "referential integrity violation", "load_date out of order", "new record_source value detected". Use a tool like dbt tests, Great Expectations, or Alteryx to codify these checks.

10 Model Validation: Physical Feasibility Checks

A logically perfect model can fail in production if the physical architecture can't sustain the workload. Index coverage for join columns, partition strategy for historical queries, row count projections, and ETL window feasibility all matter. Use ShaDey Inc.'s data volumes to validate.

Physical Feasibility Checklist (ShaDey Data Volumes)

  • Index Coverage for Query Patterns: If most queries join LINK_ORDER_PRODUCT on hub_order_hk + hub_product_hk, create a composite index: CREATE INDEX ix_link_op_order_product ON link_order_product(hub_order_hk, hub_product_hk);
  • Partition Strategy: SAT_ORDER_PAYMENT has 200M rows/year. Partition by load_date (monthly or quarterly) to enable partition pruning. Query WHERE load_date BETWEEN @start AND @end scans only relevant partitions.
  • Row Count Estimation: 50M customers × 5 Satellites (avg) × 5 versions per customer (turnover) = 1.25B rows in customer Satellites. On Snowflake, assume 1 KB per row (name, address, email, etc.) = 1.25 TB. Fits within typical 10TB warehouse.
  • Storage Projection (3-year horizon): 200M orders/year × 10 items/order = 2B order line items. 2B rows × 500 bytes (qty, price, discount, tax) = 1 TB/year. Raw Vault + Business Vault = 3 TB over 3 years. Reasonable.
  • ETL Window Feasibility: Nightly load of 10M new orders (200M/year ÷ 20 workdays). Load takes Hubs (100s ms) + Satellites (10s of seconds) + Links (minutes). Total: 5–10 min per source. Window: 2 AM–6 AM UTC allows 4-hour maintenance. Feasible.
  • Query Performance Target: A typical BI query (e.g., "revenue by customer segment") should scan the business vault + dimensional layer, not raw vault. Use materialized views for common patterns. Target: < 30 sec for any ad hoc query on 3-year history.
  • Incremental Load Efficiency: Load only NEW customer records by filtering WHERE (customer_bk, record_source) NOT IN (SELECT customer_bk, record_source FROM hub_customer). Avoid full table scans on every load.

Example: Storage Projection Calculation

Component Row Count (Annual) Bytes/Row Storage (Annual) 3-Year Total
HUB_CUSTOMER (new customers) 5M 100 500 MB 1.5 GB
SAT_CUSTOMER_DEMOGRAPHICS (versions) 25M (5M × 5 avg versions) 250 6.25 GB 18.75 GB
SAT_CUSTOMER_GOLDEN (versions) 25M 180 4.5 GB 13.5 GB
HUB_ORDER (new orders) 200M 80 16 GB 48 GB
LINK_ORDER_PRODUCT (new items) 2B 60 120 GB 360 GB
Raw Vault Subtotal 147.25 GB 442 GB
Business Vault (~40% of Raw) 59 GB 177 GB
Star Schema + Aggregates (~30% of Raw) 44 GB 133 GB
TOTAL (3 years) 250.25 GB/yr 752 GB
ShaDey Inc. can sustain this Data Vault architecture on a single Snowflake Standard edition (40 TB storage included). Cost for compute (X-Large warehouse @ $3/credit, 2 credits/hour, 10 hours daily) = ~$220/day. Well within SLA. Proceed with implementation.

11 Anti-Pattern Catalogue: Common Data Vault Mistakes

Even experienced teams can fall into traps. Here are 20 anti-patterns we've seen in the field, plus how to recognize and fix them.

P0
1. Hub Without Business Key Governance
Symptom

Multiple teams define customer business keys differently. SAP says customer_bk = SAP#. Salesforce says customer_bk = Salesforce Account ID. Shopify says customer_bk = Shopify customer ID. The Hub is loaded 3 different ways, and every query result is wrong because a single physical customer is represented as 3 separate Hub rows.

Example

SAP customer 123456, Salesforce account SF-999, Shopify customer SH-777 represent the same Acme Corp. With no governance, you create 3 Hub rows. A query "sum sales for customer Acme" returns incomplete data (only SAP orders) or triple-counts (if you UNION all sources).

Fix

Establish a Business Key Governance Registry. Define: customer_bk = CONCATENATE(SAP#, '|', SF#, '|', Shopify#). Create a mapping table (golden record). All ETL pipelines use this governance table to compute customer_bk consistently. Update governance table when a new source is added.

P1
2. Satellite Bloat: Too Many Columns in One Satellite
Symptom

SAT_CUSTOMER has 100+ columns: name, email, phone, fax, address, city, state, zip, country, industry, credit_limit, credit_utilization, segment, life_time_value, last_purchase_date, etc. When ANY column changes, a new row is inserted into a 100-column table. Storage bloats. Query performance suffers. Hashdiff becomes meaningless (detects change, but you can't tell which column changed without row-by-row inspection).

Example

A customer's email changes. A new row is inserted into SAT_CUSTOMER with 100 columns, of which only email differs. The other 99 columns are duplicated. Over 1 year, with 50M customers and 5 email changes/customer, you insert 250M rows into SAT_CUSTOMER, wasting 25 GB on redundant data.

Fix

Split SAT_CUSTOMER into logical Satellites: SAT_CUSTOMER_CONTACT_INFO (email, phone, fax), SAT_CUSTOMER_ADDRESS (address, city, state, zip), SAT_CUSTOMER_PROFILE (segment, lifetime_value, industry). Each Satellite has fewer columns, smaller rows, faster joins, and more meaningful change detection.

P1
3. Satellite With Business Logic (Should Be Business Vault)
Symptom

A Satellite contains derived columns: SAT_CUSTOMER has a column CALCULATED_CREDIT_SCORE = ROUND(lifetime_value / avg_order_value, 2). This logic should not be in Raw Vault (audit-grade, source-aligned). When the credit score calculation changes, you can't easily re-compute historical rows. Raw Vault becomes coupled to business rules.

Example

Credit score formula changes. You now need to recalculate 1M historical customer records in SAT_CUSTOMER. If you INSERT new rows, you have two versions of credit score (old formula, new formula) coexisting, causing confusion. If you UPDATE, you break immutability.

Fix

Keep Raw Vault purely source-aligned: SAT_CUSTOMER_CREDIT contains only raw score from SAP. Create a Business Vault table: SAT_CUSTOMER_CREDIT_GOLDEN, which applies the formula. When the formula changes, you re-run Business Vault; Raw Vault remains untouched. This preserves audit trail and allows formula re-computation.

P1
4. Missing Link Satellites for Time-Variant Relationships
Symptom

A supplier relationship changes: Vendor A supplies Product X at $10 (2024 Q1), then at $15 (2024 Q2). You store this in LINK_PRODUCT_VENDOR with no Satellite. A query "what was the supplier of Product X on 2024-04-01?" is ambiguous: which price applies?

Example

Historical costing is wrong. You can't replay 2024 Q1 costs because you don't know the Q1 supplier prices (they've been overwritten by Q2 prices in the Link).

Fix

Create SAT_LINK_PV_SOURCING (Product-Vendor sourcing satellite) with effective_date and expiry_date (or load_date + next_load_date). Capture price, lead_time, quality_metrics as time-variant attributes. Query WHERE effective_date <= @query_date AND (@query_date < expiry_date OR expiry_date IS NULL) to get historical values.

P2
5. Monster Dimension (Converse of Satellite Bloat)
Symptom

In the dimensional (star schema) layer, DIM_CUSTOMER has 150+ columns: all customer attributes stuffed into one dimension. This violates the principle of grain separation. DIM_CUSTOMER should be at the customer grain; if you mix in address, billing, and shipping attributes at different update frequencies, you create ambiguity.

Example

An order was shipped to address A but billed to address B. DIM_CUSTOMER has only one address column. Do you store A or B? If you store both (ship_address, bill_address), you're violating atomicity. If you pick one, reporting is wrong for the other.

Fix

Break DIM_CUSTOMER into multiple conformed dimensions: DIM_CUSTOMER (core: name, segment, lifetime_value), DIM_CUSTOMER_BILLING_ADDRESS (address, city, state, zip, billing_date), DIM_CUSTOMER_SHIPPING_ADDRESS (ship_to_address, ..., ship_date). In the fact table, include both billing_address_sk and shipping_address_sk. This is cleaner and avoids ambiguity.

P1
6. Fan Trap: One-to-Many Join Without Intermediate Fact
Symptom

A query joins FACT_SALES (one row per order) to DIM_CUSTOMER (one row per customer) and DIM_PRODUCT_CATEGORY (one category per 10 products). If a customer has 5 orders and each order has 3 products in different categories, the join creates 5 × 3 = 15 rows, inflating the fact table. SUM(sales_amount) is now 15× the actual total.

Example

Query: "Total sales by product category". You join FACT_SALES to DIM_PRODUCT (many products per category). The fact table explodes, and your sum is wrong.

Fix

Use a bridge dimension (or outrigger) if many-to-one is unavoidable. Or, re-grain the fact table: FACT_SALES_BY_CATEGORY (one row per order-category combination). Alternatively, ensure joins are 1:1 or M:1 only (never 1:M on the fact side).

P1
7. Chasm Trap: Many-to-Many Without Bridge
Symptom

A customer has many addresses (home, work, billing, shipping). A product has many categories (primary, secondary). If you join FACT_SALES to DIM_CUSTOMER to DIM_ADDRESS (M:N) to DIM_PRODUCT to DIM_CATEGORY (M:N), the row count explodes combinatorially. Aggregates are meaningless.

Example

100 addresses × 10 categories per product = 1,000 rows per fact table join. SUM(sales) is multiplied 1,000×.

Fix

Use a bridge dimension that flattens the M:N relationship. Or, create separate conformed facts: FACT_SALES_BY_DELIVERY_ADDRESS, FACT_SALES_BY_CATEGORY. Or, normalize in Data Vault: ensure Links are the source of truth; dimensions are de-normalized views.

P2
8. SCD Type 2 Without Hash Diff
Symptom

A dimension uses SCD Type 2 (effective_date, expiry_date) to track changes, but doesn't compute a hashdiff. When you load a new version of a dimension, you compare 50 columns one-by-one to detect change. If any differs, you insert a new row. This is CPU-intensive and error-prone.

Example

DIM_CUSTOMER load takes 30 minutes for 5M rows because you're comparing columns individually. A hashdiff approach would take 5 minutes.

Fix

Add a change_hash column: MD5(CONCAT(col1, col2, ..., col50)). On load, compare change_hash from staging to the previous row's change_hash. If different, insert new version. If same, skip. This reduces CPU and makes change detection explicit.

P2
9. Over-Normalization in Consumption Layer
Symptom

Your dimensional layer is so normalized that a simple report requires 15 joins. DIM_CUSTOMER references DIM_REGION, which references DIM_COUNTRY, etc. Each join adds latency. Analysts complain that queries are slow.

Example

"Total sales by region" requires joining FACT_SALES → DIM_STORE → DIM_ZONE → DIM_REGION → DIM_COUNTRY. 5 joins for one metric.

Fix

Denormalize the consumption layer for query speed. Embed region, zone, country as columns in DIM_STORE. Store is already filtered by report context (which stores?) so the denormalization doesn't inflate storage. Queries are now 1 join instead of 5. If storage is a concern, use a materialized view or aggregate table for common patterns.

P0
10. Raw Vault Write-Back (Violating Immutability)
Symptom

A downstream team finds an error in a historical order (e.g., wrong product SKU). Instead of logging the error in the source system and reloading Raw Vault, they UPDATE the Raw Vault table directly. This breaks immutability and audit trail. Future audits can't explain why the row changed.

Example

Order from 2024-01-15 had product SKU = "WRONGSKU". On 2024-06-01, someone UPDATEs it to "RIGHTSKU" in the Raw Vault. An audit query on 2024-01-20 now returns the wrong value. The timestamp of the UPDATE (2024-06-01) is lost.

Fix

Raw Vault is immutable. REVOKE UPDATE and DELETE permissions from all users. If an error is discovered, (1) log the error in the source system, (2) reload the source in Raw Vault with the corrected value and a NEW load_date, or (3) create a Business Vault correction table (SAT_CORRECTION) with reason, approved_by, approval_date. Business Vault references both Raw Vault and Corrections to derive the correct value.

P0
11. Hub Collision: Same Business Key From Different Sources
Symptom

Different source systems assign their own IDs without source qualification. SAP says customer 12345, Shopify says customer 12345 (different real customers). Both are loaded into HUB_CUSTOMER with the same business key. A query "get customer 12345" returns two unrelated customers, and reports are nonsensical.

Example

ShaDey Inc. loads SAP customer 12345 (Acme Corp) and Shopify customer 12345 (Zephyr Ltd) into the same hub. Both have customer_bk = "12345". A customer segmentation report now shows Acme and Zephyr as a single entity with combined lifetime value, destroying data integrity.

Fix

Qualify business keys by source system: customer_bk = CONCAT('SAP_', SAP_customer_id) or customer_bk = CONCAT('SHOPIFY_', shopify_customer_id). Or maintain a source-aware golden record table that maps (source_system, source_key) to a canonical customer_bk. This ensures collisions are impossible and traceability is preserved.

P1
12. Missing Effectivity Satellite for Link Relationships
Symptom

A product's supplier relationship changes: Vendor A → Vendor B → Vendor A again. LINK_PRODUCT_VENDOR records only current relationships. Historical queries "Who was the supplier on 2024-Q2?" are unanswerable. You can't recreate historical cost-of-goods-sold because you don't know supplier validity periods.

Example

Product SKU-999 was supplied by Vendor-A (Q1-Q2 2024) and Vendor-B (Q3-Q4 2024). The link is overwritten or lacks date fields. A profitability report for Q2 2024 can't determine the correct supplier cost because you don't know which vendor was active.

Fix

Create SAT_LINK_PRODUCT_VENDOR_SOURCING with effective_date, expiry_date (or dbt_load_date + dbt_next_load_date). Include supplier_cost, lead_time, quality_score. On query, filter: WHERE effective_date <= @history_date AND (@history_date < expiry_date OR expiry_date IS NULL). This preserves historical relationships and enables time-travel queries.

P1
13. Hashdiff Without Consistent Column Ordering
Symptom

Hashdiff is computed differently in different ETL environments. Dev concatenates columns as (name, email, phone), Prod concatenates as (email, name, phone). The same customer record produces different hashdiff values in dev vs. prod. Change detection fails; spurious inserts create duplicate satellite rows. Hash mismatches between environments cause reconciliation nightmares.

Example

SAT_CUSTOMER is loaded in dev with hashdiff_1 = MD5(concat(name, email, phone)). In prod, a new pipeline computes hashdiff_2 = MD5(concat(email, name, phone)). They differ for the same customer. Downstream depends-on logic breaks. Data quality checks fail.

Fix

Standardize hashdiff column order across all environments. Document it: "Hashdiff columns are alphabetically sorted." Use a dbt macro or shared SQL function that enforces this order: MD5(CONCAT(col_a, col_b, col_c, ...)) -- alphabetically. Add a test: verify hashdiff matches expected value for a known record. This prevents dev/prod misalignment.

P0
14. Business Logic in Raw Vault (Violating Load-As-Is Principle)
Symptom

Raw Vault tables contain derived, calculated, or filtered data. A satellite stores DISCOUNTED_PRICE = PRICE × (1 − discount_rate). A hub is filtered to exclude inactive customers. Raw Vault is supposed to capture source data as-is. Applying business logic before Raw Vault means (1) you can't audit source values, (2) you can't re-apply logic if business rules change, (3) you lose the single source of truth.

Example

ShaDey Inc.'s Raw Vault includes a SAT_ORDER_PRICE with FINAL_PRICE (already discounted). Later, the discount calculation changes. You can't recalculate historical rows because you've lost the original PRICE and discount_rate values. You're forced to reload the entire source.

Fix

Raw Vault is "load as-is". Capture source fields unchanged: SAT_ORDER_PRICE stores (list_price, discount_rate, load_date) without calculation. Create Business Vault tables: SAT_ORDER_PRICE_GOLDEN computes FINAL_PRICE = list_price × (1 − discount_rate). If the formula changes, regenerate Business Vault; Raw Vault remains pristine. This preserves audit trail and enables re-computation.

P1
15. Point-in-Time Table Neglect: Expensive Multi-Way Temporal Joins
Symptom

A query joins multiple satellites to reconstruct a customer's state at a point in time (e.g., "What was the customer's address, credit limit, and loyalty status on 2024-06-15?"). Without a Point-in-Time (PIT) table, this requires a 4-way join with nested subqueries filtering by load_date. Each query is slow. Analysts bypass the raw vault and build their own snapshots, fragmenting logic.

Example

A risk analyst needs to calculate customer credit exposure as of 2024-06-15. She must join SAT_CUSTOMER_CONTACT, SAT_CUSTOMER_ADDRESS, SAT_CUSTOMER_CREDIT_LIMIT (3 satellites) filtered by load_date <= 2024-06-15. The query takes 5 minutes. If she needs 100 such snapshots, that's 500 minutes.

Fix

Build PIT_CUSTOMER: one row per (customer_hk, effective_date) with foreign keys to each satellite's latest record as of that date. Pre-computed PIT tables collapse multi-way joins into a single lookup. SELECT * FROM PIT_CUSTOMER WHERE effective_date = 2024-06-15 AND customer_hk = 'xyz' returns all attributes instantly. Refresh PIT on each load. This trades storage for query speed; worth it for frequently queried entities.

P2
16. Bridge Table Misuse: Overcomplicating Simple 1:M Relationships
Symptom

A product belongs to multiple categories (primary, secondary). Instead of using a direct LINK_PRODUCT_CATEGORY, a team creates a bridge dimension (BRG_PRODUCT_CATEGORY) with weight factors. This adds complexity for a simple M:M relationship. The bridge table becomes a maintenance burden. For 1:M relationships (e.g., order has many line items), a bridge is unnecessary and adds overhead.

Example

ShaDey Inc. has a 1:M (product : category). A well-meaning architect creates BRG_PRODUCT_CATEGORY with surrogate key and weights. Now every report must join through the bridge. A simple "count products per category" query becomes a 3-table join when a 2-table join suffices.

Fix

Use bridges only when true many-to-many relationships require weighting or allocation. For 1:M, use a direct link or fact table grain. For M:M that doesn't need weighting, a simple link suffices. LINK_PRODUCT_CATEGORY (product_hk, category_hk) handles the relationship cleanly without bridge overhead. Reserve bridges for complex cases (e.g., channel attribution with channel_weight).

P1
17. Ghost Records Missing: Null Foreign Keys Break Referential Integrity
Symptom

An order references a customer that doesn't exist in HUB_CUSTOMER (e.g., source data has NULL customer_id or an orphaned key). The order row has no customer_hk, or customer_hk points to nothing. INNER JOINs silently drop these orders. Reports show wrong totals. Analysts don't know that 5% of orders are missing from customer dimension queries.

Example

An e-commerce order from an unregistered guest has customer_id = NULL. Without a ghost record, FACT_SALES has customer_hk = NULL. A report "Total sales by customer" silently excludes guest orders. Revenue is understated by 8%.

Fix

Create ghost/unknown records in each Hub: HUB_CUSTOMER with customer_bk = 'UNKNOWN_CUSTOMER' (hk_customer = -1), HUB_PRODUCT with product_bk = 'UNKNOWN_PRODUCT' (hk_product = -2). When source data has NULL or orphaned keys, map them to the ghost record. OUTER JOINs now include all rows. Use a data quality rule to alert on excessive ghost record usage (indicates source data quality issues).

P1
18. Same-As Link Confusion: Merging Distinct Records Incorrectly
Symptom

A same-as link (used for master data management) incorrectly merges two Hub records that should remain separate. Example: two customer hubs (customer_1 and customer_2) are linked as "same-as" because they share a phone number. Later, you discover they're different people (wife and husband). Undoing the same-as link is painful; downstream tables have already merged their data.

Example

Two customer records (Acme Corp and Acme Industries) are linked as "same-as" based on a partial name match. Their orders, invoices, and contacts are merged in the Business Vault. Later, you discover they're competitors in different regions. Separating them requires re-processing months of data.

Fix

Same-as links should be manually curated or algorithmically approved with high confidence thresholds. Implement a business rule: same-as links require (1) identical customer name OR (2) matching tax ID AND address. Audit same-as link changes; log who approved them and why. If a link is later revoked, maintain a SAT_SAME_AS_AUDIT (effective_date, expiry_date, approved_by, reason) so downstream can re-run Business Vault excluding the revoked link. Treat same-as as a business decision, not a data quality automation.

P1
19. Driving Key Omission: Multi-Key Links Without Designated Driving Key
Symptom

A link connects 3+ hubs (LINK_PRODUCT_SUPPLIER_WAREHOUSE) but doesn't designate a driving key. When joined with a satellite (SAT_LINK_INVENTORY_LEVEL), the effectivity logic is ambiguous. A query "which warehouse supplied product X on 2024-06-15?" can't be answered because the satellite doesn't know which key drives the relationship (product? supplier? warehouse?).

Example

LINK_PRODUCT_SUPPLIER_WAREHOUSE connects (product_hk, supplier_hk, warehouse_hk). SAT_LINK_INVENTORY_LEVEL tracks stock levels with load_date. A report "inventory levels by warehouse on 2024-06-15" requires knowing: is the satellite grain per warehouse? per product-warehouse? The ambiguity breaks temporal joins.

Fix

Designate a driving key: the entity for which the satellite is tracking changes. Document: "LINK_PRODUCT_SUPPLIER_WAREHOUSE has driving_key = warehouse_hk. SAT_LINK_INVENTORY_LEVEL is at warehouse grain (one row per warehouse per load). When joining, partition by warehouse_hk and take the latest row as of the query date." Alternatively, decompose into binary links: LINK_PRODUCT_WAREHOUSE (driving_key = warehouse) + SAT_LINK_SUPPLIER_WAREHOUSE (tracks supplier per warehouse). This clarity prevents temporal join errors.

P2
20. Immutability Enforcement Gaps: Accidental Updates Via Privilege Creep
Symptom

Raw Vault is supposed to be immutable, but over time, various teams gain UPDATE/DELETE permissions for "troubleshooting" or "data corrections". One team fixes a typo, another applies a retroactive price adjustment, a third deletes duplicate rows. Raw Vault is now mutable in practice, and audit trail is destroyed. You can't explain historical data changes.

Example

A customer service agent updates a customer phone number in Raw Vault directly (instead of correcting the source). A BI analyst deletes an erroneous sales order. An ETL developer resets a load_date. Six months later, an audit query shows contradictory results, and nobody can explain why.

Fix

Enforce immutability with database constraints: (1) REVOKE UPDATE, DELETE, TRUNCATE from all users except system administrators (required only for disaster recovery). (2) Create a correction workflow: errors are logged in a SAT_CORRECTION table with (error_description, source_record_id, correction_record_id, approved_by, correction_date). (3) Business Vault selects from Raw Vault LEFT OUTER JOIN SAT_CORRECTION to derive the corrected value. (4) Monitor database logs for any UPDATE/DELETE to Raw Vault; alert immediately. This preserves immutability and traceability.

Encode anti-pattern detection into your data quality rules. Examples: "fan trap detector" (check if fact table row counts explode after dimension joins), "hashdiff validator" (ensure all Satellites have hashdiff columns), "immutability audit" (alert on any UPDATE/DELETE to Raw Vault), "hub collision detector" (check for duplicate business keys across sources), "pit freshness monitor" (alert if PIT tables are stale). Automate checks; catch problems early.

12 Neo4j Graph for Data Vault Lineage: Tracking Provenance

Data Vault is excellent for storing data, but tracking data lineage (which Hubs feed which Satellites, which Satellites feed which dimensional tables) benefits from a graph database. Neo4j's Cypher language makes complex lineage queries readable and fast. Use Neo4j to answer: "Show me all the Satellites that feed DIM_CUSTOMER. Show me which Business Vault tables depend on SAT_CUSTOMER_ECOMMERCE. If I remove a source system, which Satellites are orphaned?"

Neo4j Data Vault Lineage Graph Structure

Nodes: HUB, SATELLITE, LINK, BIZ_SAT, DIM, FACT, SOURCE_SYSTEM, ETL_PROCESS

Relationships: LOADS_FROM (Hub ← Source), DESCRIBES (Sat → Hub), CONNECTS (Link → Hub), DERIVES_FROM (Biz Sat → Sat), POPULATES (ETL → Biz Sat), FEEDS (Biz Sat → Dim/Fact)

Cypher: Create Lineage Graph
// Create Hub nodes CREATE (h_cust:HUB {name: 'HUB_CUSTOMER', bk: 'customer_bk'}) CREATE (h_prod:HUB {name: 'HUB_PRODUCT', bk: 'product_bk'}) CREATE (h_ord:HUB {name: 'HUB_ORDER', bk: 'order_bk'}) // Create Satellite nodes CREATE (s_cust_demo:SATELLITE {name: 'SAT_CUSTOMER_DEMOGRAPHICS', hub: 'HUB_CUSTOMER', source: 'SAP_KNA1'}) CREATE (s_cust_crm:SATELLITE {name: 'SAT_CUSTOMER_CRM_PROFILE', hub: 'HUB_CUSTOMER', source: 'SALESFORCE'}) CREATE (s_cust_ec:SATELLITE {name: 'SAT_CUSTOMER_ECOMMERCE', hub: 'HUB_CUSTOMER', source: 'SHOPIFY'}) CREATE (s_cust_gold:BIZ_SAT {name: 'SAT_CUSTOMER_GOLDEN', hub: 'HUB_CUSTOMER', logic: 'golden_record_merge'}) // Create Link nodes CREATE (l_ord_prod:LINK {name: 'LINK_ORDER_PRODUCT', hubs: '[HUB_ORDER, HUB_PRODUCT]'}) // Create Dimension/Fact nodes CREATE (d_cust:DIM {name: 'DIM_CUSTOMER', grain: 'customer'}) CREATE (f_sales:FACT {name: 'FACT_SALES', grain: 'order_line'}) // Create relationships (lineage) MATCH (s:SATELLITE), (h:HUB) WHERE s.hub = h.name CREATE (s)-[:DESCRIBES]→(h) MATCH (bs:BIZ_SAT), (s:SATELLITE) WHERE bs.name = 'SAT_CUSTOMER_GOLDEN' AND s.hub = bs.hub CREATE (bs)-[:DERIVES_FROM]→(s) MATCH (bs:BIZ_SAT), (d:DIM) WHERE bs.hub = 'HUB_CUSTOMER' AND d.name = 'DIM_CUSTOMER' CREATE (bs)-[:FEEDS]→(d) MATCH (l:LINK), (f:FACT) WHERE l.name = 'LINK_ORDER_PRODUCT' AND f.name = 'FACT_SALES' CREATE (l)-[:POPULATES]→(f)

Cypher Queries: Lineage Discovery

Query 1: All Satellites feeding DIM_CUSTOMER
MATCH (sat)-[:DESCRIBES|:DERIVES_FROM]-(hub)-[:FEEDS]-(dim) WHERE dim.name = 'DIM_CUSTOMER' RETURN DISTINCT sat.name, sat.source ORDER BY sat.name;
Query 2: Impact Analysis: Which tables depend on SAP source?
MATCH (sat:SATELLITE)-[:DESCRIBES]→(hub)-[:FEEDS]→(consumption) WHERE sat.source = 'SAP_KNA1' RETURN sat.name, hub.name, consumption.name;
Query 3: Lineage path from Source to Consumption (6 hops)
MATCH (source:SOURCE_SYSTEM)-[:LOADS_FROM]→(hub) -[*1..3]→(sat)-- Satellites describing the Hub -[:FEEDS]→(biz)-- Business Vault layer -[:FEEDS]→(dim_or_fact)-- Dimensional layer WHERE source.name = 'SALESFORCE' RETURN source.name + ' → ' + hub.name + ' → ' + sat.name + ' → ' + dim_or_fact.name lineage_path;
Implement this Neo4j graph as part of your data governance platform. Auto-populate from dbt metadata or ETL logs. Query it when stakeholders ask "which tables are affected by a source outage?" or "can we retire this table?" Lineage queries that take hours in a relational database take milliseconds in Neo4j.

Summary: Data Vault for ShaDey Inc.

Data Vault 2.0 is a proven architecture for multi-source retail integration. Raw Vault captures source-aligned truth; Business Vault applies golden record logic; star schema feeds BI. Validate at three levels: business requirements, logical consistency, physical feasibility. Avoid the 20 anti-patterns. Use Neo4j for lineage governance. ShaDey Inc.'s 50M customers, 200M orders, and 12K vendors justify the investment. Build with confidence.

Next Steps: Establish business key governance. Design Hubs for each entity. Draft Satellites per source system. Validate with stakeholders. Run pilot load. Iterate. Deploy to production.