Jaco van der Laan

Modeling Data Transformations in Logical Steps

05/10/2025  ·  Business Friendly Mapping  ·  Data Modeling

lineage scd2 temporal
‹ ›

Stop Mixing Raw and Derived Data in Your Integration Layer

Summary

Too often, data teams store raw source columns and business derivations together in the same intermediate tables. This creates lineage confusion, audit headaches, and broken reproducibility when logic changes.

This article proposes a simple modeling pattern: separate derivations from pass-through data, historize every meaningful change (bi-temporal SCD2), and keep purely technical reshaping as lightweight views.

The result: clear lineage, reproducible outputs, and maintainable data platforms — while saving storage by avoiding unnecessary duplication of raw data.

🚀 Key Takeaways

  • Two transformation types: Structural reshaping (no meaning change) vs. Derivational transformations (new meaning or filters).
  • Reshape with views: Rename, cast, and split source entities without materializing.
  • Historize derivations only: Store and version only new calculations and filtered sets — don’t duplicate historized source ==data==.
  • Model step-by-step: One derivation = one transformation; avoid big monolithic SQL scripts.
  • Stable interfaces: Combine raw + derived data into clean business entities for downstream use.

A Familiar Pain Story

You’re debugging a critical report at 11 PM. The CFO asks why “active customers” counted 50k last month but only 43k now.
You open the SQL behind the table feeding the report — a 400-line script mixing raw CRM data, some mysterious filters, and a few calculated fields.

  • Where is the original customer set?
  • When did the “active” rule change?
  • Can we reproduce what was delivered last quarter?

Too often, you can’t answer. The transformation is a black box. Raw and derived data live together in one big intermediate table, so when business logic changes, history is lost and storage balloons with duplicated data.

It doesn’t have to be this way.

Two Kinds of Transformations — and Why the Distinction Matters

1. Structural / Technical Transformations (no meaning change)

Examples: column renames, data type casting, splitting one source table into multiple logical entities, key normalization.

Characteristics:

  • Idempotent: the same input always yields the same output.
  • No business logic applied.

Materialization:
Use views or ephemeral CTEs (in dbt). No history needed.

2. Derivational Transformations (change meaning)

Examples: calculating gross_margin = revenue — cost, classifying customers into segments, filtering to a curated set of primary keys, data quality adjustments or enrichment with reference data.

Characteristics:

  • Business logic is applied.
  • Results may change when rules change.

Materialization:
Use tables with bi-temporal SCD2 historization. Keep each derivation’s output versioned for auditability and reproducibility.

Storage note:
Only store and historize derivations.
Don’t copy raw, historized source data into the integration layer again — it’s already stored in your historized source layer.
The integration layer should be thin: persist only derived outputs and expose integrated business views that combine those derivations with the existing historized source data.

Relation to Data Vault Concepts

If you’re familiar with the Data Vault methodology, you’ll recognize a similar idea to the separation of the Raw Vault and Business Vault.
In this approach we deliberately don’t introduce the Raw/Business Vault

terminology — we want to avoid adding extra jargon. Instead, we logically separate transformations by their nature (pass-through vs. derivation) and use materialization settings (e.g., view, ephemeral, incremental) to achieve the right physical implementation.
The principle is the same: keep raw source history intact, and persist only the business logic transformations that change the meaning of data.

Recommended Materialization Strategy

This pattern works especially well with dbt:

  • Use ephemeral or view for structural steps.
  • Use incremental or table models for derivations with historization.
  • Build interface views that recompose raw and derived columns into a clean business entity without duplicating raw storage.

Example: Customer Data

stg_customer — historized raw source layer
reshape_customer — view: renaming & casting only
drv_customer_segment — table: SCD2 with calculated segment
drv_active_customers — table: SCD2 set of PKs meeting “active” rule
int_customer — final business-facing view combining base + derived

Notice that only the derivations (drv_*) are materialized.
The original stg_customer (already historized) stays untouched and isn’t duplicated.

A Simple dbt Setup

dbt_project.yml

models:
  my_project:
    staging:
      +materialized: view
      +tags: ["raw"]
    reshaping:
      +materialized: ephemeral
      +tags: ["raw"]
    derivations:
      +materialized: incremental
      +tags: ["derived", "historized"]
    integration:
      +materialized: view
      +tags: ["business_interface"]

Example model: models/derivations/drv_customer_segment.sql

{{ config(
    materialized='incremental',
    unique_key='customer_id',
    tags=['derived', 'historized'],
    meta={
      'business_owner': 'CRM team',
      'description': 'Calculates customer segments for marketing analytics'
    }
) }}

SELECT
    c.customer_id,
    CASE
        WHEN total_spend > 10000 THEN 'PLATINUM'
        WHEN total_spend > 5000  THEN 'GOLD'
        ELSE 'SILVER'
    END AS segment,
    CURRENT_DATE() AS valid_from
FROM {{ ref('reshape_customer') }} c

This setup keeps reshape steps light and ephemeral while ensuring every derivation is persisted and historized — without duplicating the source.

Why Historization Matters

  • Business logic changes — tax rules, product definitions, “active customer” thresholds.
  • If you don’t persist derivation results over time, you can’t explain past reports or reproduce numbers.
  • SCD2 bi-temporal tables capture both validity time and processing time, making your integration layer audit-ready — while keeping storage efficient by materializing only derivations.

How to Adopt This Pattern

  1. Naming convention: e.g., stg_, reshape_, drv_, int_.
  2. Default historization: assume every derivation will need SCD2.
  3. Meta tagging: in your catalog or dbt meta, mark models as raw vs derived.
  4. Refactor gradually: pull out derivations from monolithic SQL and materialize them.
  5. Educate your team: raw ≠ derived; don’t duplicate historized data.

Connections to Other Modern Practices

Key Benefits Recap

  • Transparent lineage
  • Easier testing and deployment
  • Full reproducibility of historical outputs
  • Faster builds (fewer unnecessary materializations)
  • Lower storage footprint — only store derivations, not duplicate historized source data
  • Clear contract between integration & semantic layers

Final Thought

Every filter or calculation is a business decision.
Business decisions deserve history — column renames don’t.
And your storage deserves efficiency — don’t copy what you already historize.

I’d love to hear from other data modelers and engineers:

  • How do you separate raw vs derived data in your transformations?
  • Do you historize derivation outputs, and if so, how do you implement it (SCD2, snapshots, versioned tables)?

What patterns have worked well (or failed) for reproducibility, lineage clarity, and storage efficiency?

Share your experience or lessons learned in the comments — let’s compare notes and refine these patterns together.