System designCore8 min

Data warehouses, lakes & pipelines

The questions a business asks cut across every system it runs: orders, app events, ads, deliveries, support. A data warehouse or lake gathers copies of all of them in one place built for analysis, and a pipeline keeps those copies fresh, cleaned, modeled and tested. Most of the engineering is in the pipeline, not the storage.

1 · The idea

Analysis needs every system's data in one place

A meal-kit subscription company wants to know which ad channel brings customers who are still subscribed six months later. The answer needs data from five systems that were never meant to meet:

Source

Holds

Shape

PostgreSQL

Customers, subscriptions, boxes ordered

Current state; old values overwritten

App event logs

Sign-ups, recipe views, skips

JSON files, billions of rows

Two ad platforms

Spend and clicks per campaign

Daily exports through their APIs

Delivery partner

Delivered, late, damaged

CSV files dropped each night

Support tool

Complaints and refunds

A SaaS API

None of these can join with another, and the database can't answer questions about the past, since a customer's plan is overwritten when it changes. Running big scans on the production database would also slow the app (OLTP vs OLAP & columnar storage). So the company copies all five into one analytical store, keeps the history, and reshapes it for questions.

2 · Warehouse or lake

A warehouse, a lake or a lakehouse holds the copy

There are three common homes for that copy, and they've been converging:

Store

Holds

Strength

Catch

Warehouse

Tables in a managed columnar SQL database

Fast SQL, managed storage

Data must be loaded into it first

Lake

Files, often Parquet, in object storage

Cheap, any format, any engine

Files alone have no transactions

Lakehouse

Lake files plus a table format

Tables with commits on cheap storage

One more layer to run

Cloud warehouses separate storage from compute. BigQuery stores tables in its own columnar format and scales storage and compute independently. Snowflake runs queries on virtual warehouses, independent compute clusters that don't affect each other's performance, so the finance team's month-end queries don't slow the dashboards.

A lake is just files, so two jobs writing at once, or a reader arriving mid-write, can see a mess. Table formats such as Apache Iceberg and Delta Lake fix that. Each change writes new files plus a new metadata file, then swaps the table's pointer to it atomically, so readers never see partial changes. Every version is a snapshot, which enables time travel and rollback to a good state.

3 · ELT and layers

Load the raw data first, then transform it in layers

The traditional order was ETL: extract from the source, transform on the way, load the cleaned result. Cloud warehouses made ELT common: load the raw data as it is, then transform it inside the warehouse with its own compute. The main reason: the raw source data stays available, so transformations can be changed and rerun later. ETL still fits industries such as finance and healthcare, where data must be validated before it enters the warehouse.

Raw (bronze)as the source sent itCleaned (silver)deduplicated, joinedMarts (gold)star schemasclean, conformmodel per useRaw (bronze)as the source sent itCleaned (silver)deduplicated, joinedMarts (gold)star schemasclean, conformmodel per use

The medallion layout. Raw keeps every load as received, so any later layer can be rebuilt. Cleaned joins sources into one view of customers and boxes. Marts are shaped for one team's reports.

The raw layer is a historical archive that allows reprocessing without re-reading the source systems. For the meal-kit company that matters: a bug in the cleaning step is fixed by rerunning it from raw, without asking the ad platforms or the delivery partner for old data again (Batch processing). Changes from PostgreSQL arrive through change data capture rather than nightly full copies (Change data capture).

4 · Star schemas

Model the marts as facts surrounded by dimensions

Marts are usually dimensional models. A fact table records measurement events, one row per event, with numeric measures and a key for each dimension. Dimension tables hold the descriptive context: who, what, where, when. Laid out with the fact in the middle and dimensions around it, the result is called a star schema:

dim_customerplan, city, signup channeldim_dateday, week, holidaydim_recipecuisine, costfact_box_deliveryone row per box deliveredcustomer_keydate_keyrecipe_key

The fact holds measures: price, discount, delivery minutes. Every question becomes a join from the fact to the dimensions it filters or groups by.

The first decision is the grain: exactly what one fact row represents. The grain is the binding contract of the design, and the usual advice is the lowest level the business process captures, one row per box delivered, not per customer per month. Summaries can always be built from detail, but detail can't be recovered from summaries, and different grains belong in different fact tables.

5 · History

Keep history on purpose, because sources overwrite it

In March, a customer named Ines switches from the two-person plan to the family plan. PostgreSQL updates the customer row; the old plan is gone. A report of revenue by plan for February would now wrongly count Ines's February boxes as family boxes. The warehouse must keep what was true when.

A type 2 slowly changing dimension does it by adding a row instead of updating one. Each version of the customer gets its own surrogate key, plus three columns: when the row became effective, when it expired, and whether it's current. New facts point to the new key, and old facts keep pointing to the old one:

dim_customer after Ines changes plan

customer_key  customer_id  plan        effective   expires     current
------------  -----------  ----------  ----------  ----------  -------
       10441  C-2207       two-person  2025-06-02  2026-03-14  no
       11893  C-2207       family      2026-03-14  9999-12-31  yes

February's boxes reference key 10441 and stay two-person; April's reference 11893. Table formats such as Delta Lake support the merge operations this needs, and also let a whole table be read as of an earlier snapshot.

6 · Pipelines

A pipeline is code, so test it before anyone sees its output

The transformations are code: in dbt, each model is a SQL SELECT, and the tool builds tables from them in dependency order. Data changes under that code every day, so the pipeline also tests the data. dbt ships four generic tests, unique, not_null, accepted_values and relationships, and any SQL query that returns failing rows can be a test of its own:

A new delivery-partner export arrived with unknown customer IDs. The tests catch it before the dashboards switch to the new build, and the fix is a rerun from raw.

Because each run can be repeated over the same raw partition, a fix or a backfill is simply another run. The company also tracks freshness: if the ad platform export hasn't arrived by 6 a.m., the morning dashboards say so instead of quietly showing yesterday's numbers.

7 · In practice

Copy everything raw, model for questions, test every run

The storage choice matters less than the habits around it. Personal data copied into the warehouse inherits every duty of the source, from access control to deletion requests (Privacy, residency & retention).

  1. Land raw data unchanged

    Append-only, partitioned by load date; it's the archive every layer rebuilds from.

  2. Transform inside the warehouse

    ELT with versioned SQL models, in layers from raw to marts.

  3. Declare the grain first

    One fact row per real event, at the lowest level captured.

  4. Keep history in dimensions

    Type 2 rows for attributes reports group by.

  5. Test and publish atomically

    Failing tests hold the old version; table formats swap in the new one at once.

Check yourself

4 questions

1. Why does ELT keep the raw data in the warehouse?
2. What does a table format such as Iceberg add to Parquet files in object storage?
3. Why declare the fact table's grain at the lowest level, one row per box delivered?
4. Ines changes plan in March. How does a type 2 dimension keep February's report right?

Takeaways

Remember this

  • A warehouse or lake gathers copies of every system's data so questions can join them and see history.
  • Warehouses are managed columnar databases; lakes are files in object storage; table formats add atomic commits and snapshots to lakes.
  • Load raw data first and transform it inside, in layers from raw to cleaned to marts, so everything can be rebuilt.
  • Model marts as facts at the lowest grain with dimensions around them, and keep dimension history with type 2 rows.
  • Pipelines are code: test the data on every run and publish only builds that pass.