Настоящий материал представляет собой обзор и практическую разработку идей из книги Питхейн Стренгхольт, «Масштабируемые данные. Лучшие шаблоны высоконагруженных архитектур» (СПб.: Питер, 2022; оригинал: Piethein Strengholt, «Data Management at Scale», O'Reilly Media, 2020).
Прямые ссылки на главы книги и её принципы приводятся с указанием источника. Остальной текст — авторская систематизация, расширение принципов до рабочего кода, сопоставление с актуальными инструментами 2026 года (PyDeequ, dbt, JSON Schema-контракты, RAG-пайплайны) и дополнительные замечания, выходящие за пределы исходного текста книги.
В отчёте HR и финансов — разные суммы по одному клиенту. В воронке продаж — другие имена, чем в CRM. Каждый, кто работал с корпоративными данными, видел это, и почти каждый винил «грязные данные». На деле причина обычно глубже: данные разошлись потому, что не существует места, где им положено быть одинаковыми.
Рассмотрим типичную сцену, знакомую почти любой компании среднего размера и старше. Клиентка Ольга, руководитель отдела закупок, меняет в CRM рабочий телефон. Через месяц маркетинг получает телефоны из выгрузки ERP — со старым номером, потому что синхронизация в ERP шла из CRM, но поправку внесли в интерфейс CRM «для текущей кампании», минуя API. Через квартал финансовый сервис сверяет остатки по договору с биллингом — и находит расхождение в 4 тысячи рублей: биллинг считает по событию активации, ERP — по данным накладной, а обе системы «владели» датой активации. Отчётность собирается из копий, копии расходятся, а расхождения всплывают в самых дорогих местах — на сверке с контрагентом или на налоговой проверке.
Питхейн Стренгхольт (главный архитектор данных ABN AMRO) в книге «Масштабируемые данные» называет место, где этот сценарий останавливается: золотой источник — авторитетное приложение, в котором все достоверные данные создаются и обрабатываются в определённом ограниченном контексте. А золотой набор данных — подлинные, уникальные и точные данные, которые из него выходят. Это не маркетинговые термины: это контракт между системами, без которого распределённая архитектура данных деградирует в «двухточечный ад» — десятки копий одного и того же факта, каждая со своей версией правды.
В этой статье — семь принципов Стренгхольта (в первой редакции обзора их было шесть, седьмой заслужил отдельного слова), контракт на поставку данных и рабочий код: валидатор доставки, который ловит нарушение контракта до того, как испорченные данные попадут в аналитику. Плюс — то, чего в книге нет, а в 2026-м стало стандартом: проверки качества через PyDeequ и dbt, политика версионирования контрактов и прямой мост к RAG-пайплайнам.
Формулировки из главы 2 книги, сгущённые до сути — с анти-паттерном к каждому.
Данные меняются там, где создаются, и с одобрения владельца — не в копиях и не в отчётах.
Аналитик «подправил» статус клиента прямо в витрине, потому что источник недоступен. Через месяц витрина и источник уже рассказывают разные истории, и никто не помнит, кто что правил.
Если правку нельзя внести в источник — значит, либо у источника нет API, либо у правки нет владельца. Обе проблемы решаются дороже, если тянуть их месяцами.
Из системы-источника уходят оригинальные данные, а не их очередные интерпретации.
Команда отчётности просит источник «отдать данные уже агрегированными, как нам удобно». Через полгода агрегатное правило меняется — а под него подпадают восемь витрин, построенных на старой логике.
Набор, охватывающий несколько систем, — не золотой, а вопрос о его владельце открыт навсегда.
«Сводный реестр клиентов» из CRM, биллинга и ERP, который завели как общий справочник. При каждом расхождении систем начинается спор, чьё значение правильное — и это спор о данных, у которых нет владельца.
Метаданные о владельцах доступны всем — иначе прозрачность невозможна.
Владение описано в вики-странице, которую редактировали в последний раз три года назад; актуальный владелец живёт в голове инженера, который уволился.
Фильтрация, переименование полей, агрегирование — синтаксические преобразования, владелец не меняется. Как только данные соединены в новый факт — новый контекст, новый владелец.
«LTV-клиента» построили соединением биллинга, логов и CRM — и никто не назначил владельца факта LTV, потому что «это же просто join». Когда маркетинг и финансы получили разные LTV, разбираться стало некому.
Именно принцип 5 отвечает на вопрос «кто отвечает за эту витрину»: не за таблицу, а за факт.
Потребитель, который раздаёт данные дальше, становится поставщиком — и обязан соблюдать те же правила.
Команда А отдала выгрузку команде Б «просто посмотреть», команда Б отлила её в общий слой для команды В. Через два года цепочка из четырёх передач не имеет ни владельца, ни метаданных — а данные из неё уже ушли в ML-модель и в партнёрский кабинет.
Неуправляемые копии золотых наборов дороги в обслуживании и плохо контролируются. Хранить данные у потребителя оправдано только тогда, когда нужно сохранить вновь созданные данные.
Каждый аналитический проект начинается с «сделаем свою копию, чтобы никого не дёргать» — и организация накапливает слой копий, ни одна из которых не обновляется с момента создания.
Книга добавляет к этим принципам восьмой — про контракт, и к нему мы сейчас перейдём, потому что контракт — единственный из принципов, который можно проверить программой.
Золотой источник — авторитетное приложение, в котором факты рождаются и изменяются по бизнес-правилам. Золотой набор — подлинные данные, которые из него выходят, и они всегда подмножество одного приложения.
Принципы 1–7 — это не технические решения, а контракт между командами. Их нельзя купить инструментом — только договориться и зафиксировать.
Единственный из принципов, который можно проверить программой, — восьмой, про контракт на поставку. Он и становится минимальной точкой входа.
Самый частый вопрос на этом месте: «хорошо, а чья система станет золотым источником?» Стренгхольт честен: «поиск надёжных приложений порой непрост» — и допускает исключения, когда золотой набор не управляется той системой, которая фактически создала данные. Разберём три типичных спора и как их решает модель.
У компании есть биллинг (создаёт факты) и витрина отчётности (агрегирует красивее). Витрина удобнее — но удобство не создаёт владения. Критерий один: где факт рождается и где его изменяют бизнес-правила — там и золотой источник.
Витрина может владеть только своими новыми фактами (например, «рейтинг клиента по нашей методике»), а не чужими датами активации.
Дату активации создаёт биллинг, но сумма договора живёт в ERP. Классическая ошибка — назначить владельцем «систему, где поле называется красивее».
Правильный разбор: поле «сумма договора» в биллинге — это копия факта из ERP, и если она попадает в золотой набор биллинга, она должна тянуть за собой происхождение: ссылку на элемент из ERP и одобрение владельца. Форма таблицы не создаёт владения — только место рождения факта.
Старая система работает двадцать лет, новое приложение элегантно, но не покрыто интеграциями. Стренгхольт предлагает ролевую классификацию: приложение помечается как золотой источник или как DDS (хранилище предметных данных), и классификация — это не позорная метка, а явное распределение ответственности.
DDS, который сам создаёт новые факты, становится новым золотым источником — то есть миграция возможна, но она проходит через явное назначение владения, а не через тишину.
Отдельно важен переход «потребитель → поставщик». Если интегрированные данные нужно раздать в другие контексты, потребитель становится поставщиком: данные возвращаются на уровень данных, и он обязан соблюдать все критерии поставщика. При этом Стренгхольт советует различать золотые данные и интегрированные данные: происхождение интегрированных показывает все зависимости от чужих областей, а распространение требует одобрения владельцев оригиналов.
Стренгхольт пишет, что поставка и потребление данных обеспечиваются контрактом: условия использования, SLA, версии и совместимость интерфейсов. В главе 3 он даёт пример дескриптора в формате CSVW — XML-файл, описывающий владельца, контакт, формат, размеры и контрольные суммы фрагментов. Концептуально это было на три года впереди — но к 2026 году такой дескриптор должен уметь читать любой пайплайн, и XML здесь уже не лучший выбор.
Модернизируем дескриптор книги до JSON Schema-контракта — сохранив все его поля и добавив то, чего в 2020-м ещё не было: машиночитаемые пороги качества и классификацию.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "crm_customer_data",
"contract_version": "1.2.0",
"owner": {
"id": 63845,
"contact": "John Snow <johnsnow@example.com>",
"department": "HR"
},
"golden_source": { "application_id": 23, "application_name": "CRMR" },
"delivery": {
"format": "csv",
"compression": "gzip",
"separator": "\t",
"chunks": [
{ "location": "data_1.csv", "size": 1234, "checksum_sha256": "…" }
]
},
"sla": {
"uptime": "99.5%",
"max_error_rate": "0.1%",
"refresh": "daily 03:00 UTC",
"deprecation_notice_days": 90
},
"usage_terms": ["production", "test"],
"classification": "sensitive-pii",
"quality": {
"completeness_min": 0.98,
"uniqueness_keys": ["customer_id"]
},
"schema": [
{ "name": "customer_id", "type": "int", "nullable": false, "golden_element": 987, "sensitive": false },
{ "name": "first_name", "type": "string", "nullable": true, "golden_element": 988, "sensitive": true }
]
}
golden_element — связь каждого поля с зарегистрированным элементом данных (центральный реестр из принципа 4).
classification — классификация на уровне контракта, а не только на уровне отдельного поля.
quality — числовые пороги, по которым поставка принимается или отклоняется автоматически.
Контракт без политики версионирования — это обещание, которое забывают. Рабочее правило заимствуем из API-мира (глава 4 книги о совместимости интерфейсов — ровно об этом):
deprecation_notice_days (у нас — 90 дней), потом отмирает.Механика deprecation важнее самой нумерации: при выпуске major-версии контракт публикуется в двух версиях одновременно, валидатор приёмки подхватывает обе, а в метаданные delivery-канала ставится deprecation_after — дата, после которой старая версия отвергается с внятной ошибкой, а не с загадочным падением джоба. Тот пункт чек-листа про «потребители узнают о поломке по падению джобов» закрывается ровно этим механизмом — и это пятнадцать строк конфигурации, а не квартал работ.
Ключевая идея книги — качество контролируется в источнике или в RDS, до приёма данных, а не постфактум в дашборде. И это связано с другим принципом книги: RDS неизменяемы и доступны только для чтения — из них нельзя восстановить разные версии истины для одних и тех же данных, а «истину» можно доработать только в золотом источнике. Если поставку не проверить на границе, испорченные данные осядут в неизменяемом хранилище, и вернуть их можно только перестройкой — то есть дороже в десять раз.
Ниже — валидатор, который проверяет поставку на соответствие контракту: формат, размеры, контрольные суммы, схему, типы, обязательность полей. При нарушении — отчёт владельцу и отказ, ровно как в «службе квитирования» из главы 6 книги.
#!/usr/bin/env python3
"""Валидатор доставки данных по контракту.
Использование:
python3 validate_delivery.py delivery/ data_contract.json
Коды возврата: 0 — принята, 1 — отклонена с отчётом.
"""
import csv, hashlib, json, os, sys
from datetime import datetime
def sha256(path):
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
h.update(chunk)
return h.hexdigest()
def fail(check, msg):
print(f"[FAIL] {check}: {msg}")
return False
def ok(check, msg):
print(f"[ OK ] {check}: {msg}")
return True
def parse_type(value, declared):
if declared == "int":
int(value); return
if declared == "float":
float(value); return
if declared == "date":
datetime.strptime(value, "%Y-%m-%d"); return
return # string — только nullable-проверка
def main(delivery_dir, contract_path):
with open(contract_path, encoding="utf-8") as f:
c = json.load(f)
verdict = True
print(f"Контракт: {c['title']} v{c['contract_version']}, "
f"владелец #{c['owner']['id']} ({c['owner']['department']})")
print("-" * 70)
# 1. Формат поставки
if c["delivery"]["format"] != "csv":
verdict &= fail("delivery.format",
f"поддержан только csv, в контракте {c['delivery']['format']}")
else:
ok("delivery.format", "csv")
# 2. Контрольные суммы: файл не менялся после подписания контракта
declared = {ch["location"]: ch for ch in c["delivery"]["chunks"]}
found = set()
for fname in sorted(os.listdir(delivery_dir)):
if not fname.endswith(".csv"):
continue
found.add(fname)
path = os.path.join(delivery_dir, fname)
ch = declared.get(fname)
if ch is None:
verdict &= fail("delivery.chunks", f"{fname} не объявлен в контракте")
continue
if os.path.getsize(path) != ch["size"]:
verdict &= fail("size", f"{fname}: {os.path.getsize(path)} против {ch['size']}")
else:
ok("size", fname)
actual = sha256(path)
if actual != ch["checksum_sha256"]:
verdict &= fail("checksum", f"{fname}: изменился после подписания ({actual[:12]}…)")
else:
ok("checksum", fname)
for missing in set(declared) - found:
verdict &= fail("delivery.chunks", f"{missing} объявлен, но отсутствует")
# 3. Схема: колонки, типы, nullability
rows, header = [], None
for fname in sorted(f for f in os.listdir(delivery_dir) if f.endswith(".csv")):
with open(os.path.join(delivery_dir, fname), encoding="utf-8") as fh:
r = csv.DictReader(fh, delimiter=c["delivery"]["separator"])
header = header or r.fieldnames
rows.extend(r)
expected = [s["name"] for s in c["schema"]]
if header != expected:
verdict &= fail("schema.columns", f"ожидались {expected}, получены {header}")
else:
ok("schema.columns", "совпадают с контрактом")
errors = []
for i, row in enumerate(rows):
for s in c["schema"]:
v = row.get(s["name"])
if v in (None, "") and not s["nullable"]:
errors.append(f"строка {i+1}: пусто в обязательном поле {s['name']}")
elif v:
try:
parse_type(v, s["type"])
except ValueError:
errors.append(f"строка {i+1}: {s['name']}={v!r} не {s['type']}")
if errors:
verdict &= fail("schema.checks", f"{len(errors)} нарушений: {errors[:3]}…")
else:
ok("schema.checks", "типы и обязательность в порядке")
if rows:
print(f"[ OK ] delivery.rows: принято {len(rows)} строк")
print("-" * 70)
print("ПОСТАВКА ПРИНЯТА" if verdict
else "ПОСТАВКА ОТКЛОНЕНА — верните отчёт владельцу (принцип 8)")
sys.exit(0 if verdict else 1)
if __name__ == "__main__":
if len(sys.argv) != 3:
print(__doc__); sys.exit(2)
main(sys.argv[1], sys.argv[2])
Чистая поставка:
[ OK ] checksum: data_1.csv [ OK ] checksum: data_2.csv [ OK ] schema.checks: типы и обязательность в порядке ПОСТАВКА ПРИНЯТА
А теперь кто-то дописал в data_2.csv строку после подписания контракта:
[FAIL] size: data_2.csv: 1552 байт против 1525 в контракте [FAIL] checksum: data_2.csv: изменился после подписания (0627eb4c4058…) ПОСТАВКА ОТКЛОНЕНА — верните отчёт владельцу (принцип 8)
Обратите внимание: схема при этом осталась корректной — колонки на месте, типы сходятся. Контрольная сумма ловит то, что не ловит ни одна проверка схемы: подмену содержимого. Именно поэтому в дескрипторе Стренгхольта checksum появился раньше, чем в большинстве реальных конвейеров.
Валидатор проверяет доставку, а не содержимое фактов. Он не узнает, что «дата активации» у биллинга и ERP указывает на разные события — для этого нужны проверки качества следующего уровня.
Схема и целостность — технический уровень проверки. Дальше идут функциональные показатели качества: полнота, уникальность, согласованность. Стренгхольт рекомендует Deequ (AWS) и Delta Lake — Spark-фреймворк прогоняет сотни миллионов строк за минуты. Вот как выглядит набор проверок на PyDeequ, соответствующий порогам из контракта:
from pyspark.sql import SparkSession
from pydeequ.checks import Check, CheckLevel
from pydeequ.verification import VerificationSuite
spark = SparkSession.builder.appName("quality-gate").getOrCreate()
df = spark.read.parquet("crm_customer_data/")
check = Check(spark, CheckLevel.Error, "contract-1.2.0")
result = VerificationSuite(spark) \
.onData(df) \
.addCheck(
check.hasSize(lambda x: x > 0) # поставка не пустая
.isComplete("customer_id") # обязательность
.isUnique("customer_id") # uniqueness_keys
.hasCompleteness("first_name", lambda x: x >= 0.98) # completeness_min
.isContainedIn("status", ["active", "frozen", "closed"])
) \
.run()
result_df = VerificationSuite(spark).onData(df).run()
check_result_as_dataframe(spark, result_df)
Здесь уместен ИИ-ассистент — но с оговоркой, которую нельзя опускать. Промт, который по описанию домена генерирует каркас проверок, работает так:
«Ты — инженер по качеству данных. Вот описание домена: [описание]. Контракт полей: [schema]. Сгенерируй: (1) Deequ-проверки на Scala/PyDeequ — uniqueness для ключей, completeness для обязательных полей, referential integrity для связей; (2) dbt-тесты из того же набора; (3) для каждой проверки — причину, почему она нужна.»
Сгенерированное — это черновик, а не готовый контроль. LLM уверенно напишет вам проверку на уникальность first_name — и формально будет права, а фактически сломает процесс, потому что одноимённых Ивановых много. Без ревью владельцем домена вы получите аккуратно упакованный мусор с высокой уверенностью.
Правильный порядок: генерация → ревью владельца (принцип 5 — он и есть владелец этих фактов) → включение в контракт.
Отдельно стоит сказать про самый дешёвый уровень проверок, который часто пропускают: сверку метаданных друг с другом. Стренгхольт в главе 7 прямо рекомендует: «для проверки общей целостности метаданных управление данными должно использовать те же инструменты качества данных, которые применяются для профилирования любых других данных». Проще говоря: реестр золотых источников — тоже данные, и он ломается так же, как ломается ваша витрина. Если в реестре два владельца у одного набора или набор привязан к несуществующему приложению — это должно ронять CI, а не ждать годовой инвентаризации.
Принцип 4 требует централизованной регистрации, но что это значит на практике? У Стренгхольта это конкретные компоненты, и они переживают любой стек:
Автономный репозиторий, который хранит общую информацию обо всех уникальных данных и их принадлежности. Важная деталь: регистрация ведётся на уровне логических наборов, потому что отношения между наборами и приложениями — «многие ко многим»: у одного приложения может быть несколько владельцев данных, а несколько физических наборов — происходить из одного приложения.
Центральное хранилище всех приложений и их владельцев (в книге — вплоть до ServiceNow): кто развёрнут, где продаётся, какой у него рейтинг конфиденциальности, целостности и доступности. Это второй край связи: элемент данных указывает на приложение, приложение — на владельца.
Из главы 9: когда объект становится предметом MDM, мастер-идентификаторы возвращаются назад в системы-источники и сохраняются там; альтернатива — таблица поиска, которая описывает, какие локальные данные каким мастер-данным принадлежат, и публикуется в MDM. Без этой таблицы каждая сверка — ручная археология.
Минимальная работающая версия реестра для организации, которая только начинает: JSON-файл в git-репозитории со списком наборов (владелец, приложение, контракт, классификация), проверки целостности в CI и процедуру изменения через pull request. Это принципиально проще каталогов-монстров и, что важно, честно отражает принцип 5: изменение владельца — это явное, обсуждаемое решение, а не тихая правка в UI каталога.
Вернёмся к Ольге из первого абзаца. С реестром её история решается за пятнадцать минут: поиск по элементу phone_work показывает, что золотой набор crm_customer_data принадлежит владельцу из CRM, поставка проверена checksum, а витрина маркетинга получила данные по принципу 6 — с одобрения владельца. Правка «минуя API» видна в origin — это нарушение принципа 1, и исправляется она в источнике, а не в копии.
Всё сказанное напрямую бьёт по RAG и LLM-пайплайнам. Retrieval, который индексирует «что-то похожее на золотой набор» — выгрузку из копии копии, — производит тихие галлюцинации: модель уверенно отвечает по данным, которые уже разошлись с первоисточником.
Разберём на той же истории с Ольгой. Агент поддержки получает вопрос: «какой у клиента телефон для экстренной связи?» В индексе — три фрагмента: из витрины маркетинга (старый номер), из выгрузки HR (без номера), из золотого набора CRM (актуальный). Ранжирование по релевантности не знает, что один из фрагментов «чище» — оно сортирует по текстовому совпадению с запросом. Модель с вероятностью, зависящей от формулировки, возьмёт старый номер — и ответит уверенно, без всяких оговорок.
Правило отсюда простое, и оно формулируется через ту же модель: индексируйте только золотые наборы или данные с контрактной гарантией свежести и происхождения — и храните в метаданных чанка golden_source и contract_version, чтобы можно было ответить на вопрос «из какой версии фактов этот ответ».
golden_source_id, dataset_id (из LoGS), contract_version, classification, refresh_at. Пять полей — и вопрос «на что ссылается ваш ответ» перестаёт быть философией.
Чек-лист диагностирует поломку; для управления изменениями нужны метрики, которые не устареют вместе со стеком:
Сколько процентов наборов в LoGS имеют назначенного владельца и живой контакт. Целевое значение для первого полугодия — 80%; не достигли — проблема в процессе регистрации, а не в инструментах.
Передач золотых наборов без одобрения владельца. Каждое обнаружение — это ещё и вопрос к происхождению: как передача вообще осталась незамеченной?
От обнаружения («в отчётах разные суммы») до исправления в золотом источнике. Растёт — значит, происхождение не работает или владельца нельзя найти.
Валидатор, Deequ, dbt-тесты — против проверенных глазами. Это единственная метрика из четырёх, которая почти не требует культурных усилий — только автоматизации.
Эти метрики намеренно скромные. Стренгхольт не обещает, что архитектура построится за квартал; его книга — про то, как не построить очередную медленноразлагающуюся централизованную монолитную копию, а вырастить распределённую систему, в которой каждая копия отвечает за своё существование.
Диагностика «у вас сломано, если…» — теперь с рецептом к каждому пункту:
distribution_approved в контракте и проверка валидатором при выходе в новый контекст.
deprecation_after в метаданных канала.
Каждый пункт — прямое следствие одного из семи принципов. Прошли по чек-листу с четырьмя «да» и больше — архитектура данных уже не масштабируется, сколько бы инструментов вы ни купили.
Эта статья — первая из трёх, построенных на книге. Следующая — дерево решений по трём архитектурам интеграции (RDS / API / стриминг): когда какой канал, какие анти-паттерны и как автоматизировать выбор. Третья — метаданные как клей: каталог, происхождение, самообслуживание. До встречи в следующем материале — и присылайте свои истории про «кто владеет этим фактом», они живее любого учебного примера.
Материал Стренгхольта — про то, что архитектура данных начинается не с инструментов, а с договорённостей о владении. Золотой источник — это не про технологии, а про то, что у каждого факта есть место рождения и владелец, который отвечает за его изменения. Контракт — единственная часть этой модели, которую можно проверить программой, и именно с него проще всего начать: валидатор на 100 строк ловит подмену содержимого, которую не ловит ни одна проверка схемы.
Расширения 2026 года — PyDeequ, dbt-тесты, JSON Schema-контракты, метаданные чанков RAG — не отменяют принципов, а дают им рабочий интерфейс. Главный вывод: инвестиции в договорённости о владении окупаются быстрее, чем инвестиции в очередной каталог. Каталог без владельцев — это витрина без товара.
Источники: 1. Стренгхольт П. «Масштабируемые данные. Лучшие шаблоны высоконагруженных архитектур» — СПб.: Питер, 2022 (главы 2, 3, 6, 7, 9). Оригинал: Piethein Strengholt, «Data Management at Scale», O'Reilly Media, 2020. — принципы 1–8, контракт на поставку, CSVW-дескриптор, неизменяемость RDS, List of Golden Sources, различение золотых и интегрированных данных, таблицы соответствия мастер-идентификаторов. 2. AWS Labs, Deequ — открытый фреймворк проверки качества данных. 3. Delta Lake, валидация схемы и качества — в книге рекомендованы для проверки поставок. 4. dbt Labs, тесты моделей и колонок — современный стандарт declarative-проверок, о котором книга ещё не могла знать.
This material is a review and practical development of ideas from Piethein Strengholt, «Data Management at Scale» (O'Reilly Media, 2020; Russian translation: «Масштабируемые данные», Питер, 2022).
Direct references to the book's chapters and principles are attributed to the source. The rest of the text is the author's systematization, extension of the principles to working code, comparison with current 2026 tools (PyDeequ, dbt, JSON Schema contracts, RAG pipelines), and additional observations beyond the original book.
HR and finance reports show different totals for the same customer. The sales funnel lists different names than the CRM. Anyone who has worked with enterprise data has seen it, and almost everyone blamed "dirty data." The real cause is usually deeper: data diverged because there's no single place where it's supposed to be identical.
Consider a typical scene familiar to almost any mid-sized or older company. Olga, a procurement manager, changes her work phone in the CRM. A month later, marketing pulls phones from an ERP export — with the old number, because ERP was syncing from CRM but the correction was made in the CRM UI "for the current campaign," bypassing the API. A quarter later, the finance service reconciles contract balances with billing — and finds a 4,000-ruble discrepancy: billing counts by activation event, ERP by invoice data, and both systems "owned" the activation date.
Piethein Strengholt (Chief Data Architect at ABN AMRO) in «Data Management at Scale» names the place where this scenario stops: the golden source — the authoritative application where all reliable data is created and processed within a specific bounded context. And the golden dataset — the authentic, unique and accurate data that leaves it. These are not marketing terms: they are a contract between systems, without which a distributed data architecture degrades into "two-point hell" — dozens of copies of the same fact, each with its own version of the truth.
This article covers Strengholt's seven principles, a delivery contract, and working code: a delivery validator that catches contract violations before corrupted data reaches analytics. Plus what the book couldn't know but has become standard by 2026: quality checks via PyDeequ and dbt, a contract versioning policy, and a direct bridge to RAG pipelines.
Formulations from Chapter 2 of the book, distilled to the essence — with an anti-pattern for each.
Data changes where it's created, with the owner's approval — not in copies or reports.
An analyst "fixed" a customer status directly in the mart because the source was unavailable. A month later the mart and the source tell different stories, and no one remembers who changed what.
Original data leaves the source system, not yet another interpretation of it.
The reporting team asks the source to "deliver data pre-aggregated, the way we like it." Six months later the aggregation rule changes — and eight marts built on the old logic fall under it.
A dataset spanning multiple systems is not golden — and its owner will forever be an open question.
A "consolidated customer registry" from CRM, billing and ERP, set up as a shared directory. At every discrepancy between systems, an argument starts about whose value is correct — an argument about data that has no owner.
Metadata about owners is available to everyone — otherwise transparency is impossible.
Ownership is described in a wiki page last edited three years ago; the actual owner lives in the head of an engineer who has left.
Filtering, renaming fields, aggregating — syntactic transformations, the owner doesn't change. Once data is joined into a new fact — new context, new owner.
A "customer LTV" was built by joining billing, logs and CRM — and nobody assigned an owner to the LTV fact, because "it's just a join." When marketing and finance got different LTVs, there was no one to sort it out.
Principle 5 answers the question "who owns this mart": not the table, but the fact.
A consumer that passes data further becomes a supplier — and must obey the same rules.
Team A handed an export to Team B "just to look at," Team B dumped it into a shared layer for Team C. Two years later, a chain of four hops has neither owner nor metadata — and data from it has already flowed into an ML model and a partner portal.
Unmanaged copies of golden datasets are expensive to maintain and poorly controlled. Storing data at the consumer is justified only when newly created data must be persisted.
Every analytics project starts with "let's make our own copy so we don't bother anyone" — and the organization accumulates a layer of copies, none of which has been updated since creation.
The book adds an eighth principle — the contract — and we'll get to it now, because the contract is the only principle that can be checked by a program.
A golden source is the authoritative application where facts are born and changed by business rules. A golden dataset is the authentic data that leaves it, and it's always a subset of one application.
Principles 1–7 are not technical solutions but a contract between teams. They cannot be bought as a tool — only agreed upon and fixed.
The only principle that can be checked by a program is the eighth — the delivery contract. It becomes the minimum entry point.
The most frequent question here: "fine, which system becomes the golden source?" Strengholt is honest: "finding reliable applications is sometimes difficult" — and allows exceptions where a golden dataset is not managed by the system that actually created the data. Let's examine three typical disputes and how the model resolves them.
The company has billing (creates facts) and a reporting mart (aggregates more nicely). The mart is more convenient — but convenience doesn't create ownership. The criterion is one: where the fact is born and where its business rules are changed — that's the golden source.
The mart can only own its own new facts (e.g., "customer rating by our methodology"), not someone else's activation dates.
The activation date is created by billing, but the contract amount lives in ERP. The classic mistake is to appoint "the system where the field sounds nicer" as the owner.
The correct parsing: the "contract amount" field in billing is a copy of a fact from ERP, and if it lands in billing's golden dataset, it must drag its provenance with it: a reference to the ERP element and the owner's approval. A table's shape doesn't create ownership — only the place of a fact's birth does.
The old system has run for twenty years, the new application is elegant but not covered by integrations. Strengholt proposes role-based classification: an application is labeled a golden source or a DDS (domain data store), and classification is not a shameful label but an explicit distribution of responsibility.
A DDS that itself creates new facts becomes a new golden source — migration is possible, but it passes through explicit ownership assignment, not silence.
The "consumer → supplier" transition is separately important. If integrated data must be distributed to other contexts, the consumer becomes a supplier: the data returns to the data layer, and it must comply with all supplier criteria. Strengholt also advises distinguishing golden data from integrated data: integrated data's provenance shows all dependencies on other domains, and distribution requires approval from the original owners.
Strengholt writes that data delivery and consumption are ensured by a contract: usage terms, SLA, versions, and interface compatibility. In Chapter 3 he gives an example descriptor in CSVW format — an XML file describing owner, contact, format, sizes, and chunk checksums. Conceptually this was three years ahead — but by 2026 any pipeline should be able to read such a descriptor, and XML is no longer the best choice.
Let's modernize the book's descriptor to a JSON Schema contract — preserving all its fields and adding what didn't exist in 2020: machine-readable quality thresholds and classification.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "crm_customer_data",
"contract_version": "1.2.0",
"owner": {
"id": 63845,
"contact": "John Snow <johnsnow@example.com>",
"department": "HR"
},
"golden_source": { "application_id": 23, "application_name": "CRMR" },
"delivery": {
"format": "csv",
"compression": "gzip",
"separator": "\t",
"chunks": [
{ "location": "data_1.csv", "size": 1234, "checksum_sha256": "…" }
]
},
"sla": {
"uptime": "99.5%",
"max_error_rate": "0.1%",
"refresh": "daily 03:00 UTC",
"deprecation_notice_days": 90
},
"usage_terms": ["production", "test"],
"classification": "sensitive-pii",
"quality": {
"completeness_min": 0.98,
"uniqueness_keys": ["customer_id"]
},
"schema": [
{ "name": "customer_id", "type": "int", "nullable": false, "golden_element": 987, "sensitive": false },
{ "name": "first_name", "type": "string", "nullable": true, "golden_element": 988, "sensitive": true }
]
}
golden_element — links each field to a registered data element (the central registry from Principle 4).
classification — classification at the contract level, not just per field.
quality — numeric thresholds by which a delivery is accepted or rejected automatically.
A contract without a versioning policy is a promise that gets forgotten. We borrow the working rule from the API world (Chapter 4 of the book on interface compatibility is exactly about this):
deprecation_notice_days (90 days here), then dies.The deprecation mechanics matter more than the numbering itself: when a major version is released, the contract is published in two versions simultaneously, the acceptance validator picks up both, and a deprecation_after date is set in the delivery channel metadata — a date after which the old version is rejected with a clear error, not with a mysterious job failure.
The key idea of the book: quality is controlled in the source or in the RDS, before data acceptance, not post-factum in a dashboard. It's tied to another principle of the book: RDSs are immutable and read-only — you cannot restore different versions of the truth for the same data from them, and "truth" can only be amended in the golden source. If a delivery is not checked at the boundary, corrupted data settles into the immutable store, and the only way to fix it is a rebuild — ten times more expensive.
Below is a validator that checks a delivery against the contract: format, sizes, checksums, schema, types, field nullability. On violation — a report to the owner and a rejection, exactly as in the "acknowledgement service" from Chapter 6 of the book.
#!/usr/bin/env python3
"""Delivery validator against a contract.
Usage:
python3 validate_delivery.py delivery/ data_contract.json
Exit codes: 0 — accepted, 1 — rejected with a report.
"""
import csv, hashlib, json, os, sys
from datetime import datetime
def sha256(path):
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
h.update(chunk)
return h.hexdigest()
def fail(check, msg):
print(f"[FAIL] {check}: {msg}")
return False
def ok(check, msg):
print(f"[ OK ] {check}: {msg}")
return True
def parse_type(value, declared):
if declared == "int":
int(value); return
if declared == "float":
float(value); return
if declared == "date":
datetime.strptime(value, "%Y-%m-%d"); return
return # string — nullable check only
def main(delivery_dir, contract_path):
with open(contract_path, encoding="utf-8") as f:
c = json.load(f)
verdict = True
print(f"Contract: {c['title']} v{c['contract_version']}, "
f"owner #{c['owner']['id']} ({c['owner']['department']})")
print("-" * 70)
# 1. Delivery format
if c["delivery"]["format"] != "csv":
verdict &= fail("delivery.format",
f"only csv supported, contract has {c['delivery']['format']}")
else:
ok("delivery.format", "csv")
# 2. Checksums: file unchanged since contract signing
declared = {ch["location"]: ch for ch in c["delivery"]["chunks"]}
found = set()
for fname in sorted(os.listdir(delivery_dir)):
if not fname.endswith(".csv"):
continue
found.add(fname)
path = os.path.join(delivery_dir, fname)
ch = declared.get(fname)
if ch is None:
verdict &= fail("delivery.chunks", f"{fname} not declared in contract")
continue
if os.path.getsize(path) != ch["size"]:
verdict &= fail("size", f"{fname}: {os.path.getsize(path)} vs {ch['size']}")
else:
ok("size", fname)
actual = sha256(path)
if actual != ch["checksum_sha256"]:
verdict &= fail("checksum", f"{fname}: changed after signing ({actual[:12]}…)")
else:
ok("checksum", fname)
for missing in set(declared) - found:
verdict &= fail("delivery.chunks", f"{missing} declared but missing")
# 3. Schema: columns, types, nullability
rows, header = [], None
for fname in sorted(f for f in os.listdir(delivery_dir) if f.endswith(".csv")):
with open(os.path.join(delivery_dir, fname), encoding="utf-8") as fh:
r = csv.DictReader(fh, delimiter=c["delivery"]["separator"])
header = header or r.fieldnames
rows.extend(r)
expected = [s["name"] for s in c["schema"]]
if header != expected:
verdict &= fail("schema.columns", f"expected {expected}, got {header}")
else:
ok("schema.columns", "match the contract")
errors = []
for i, row in enumerate(rows):
for s in c["schema"]:
v = row.get(s["name"])
if v in (None, "") and not s["nullable"]:
errors.append(f"row {i+1}: empty in required field {s['name']}")
elif v:
try:
parse_type(v, s["type"])
except ValueError:
errors.append(f"row {i+1}: {s['name']}={v!r} not {s['type']}")
if errors:
verdict &= fail("schema.checks", f"{len(errors)} violations: {errors[:3]}…")
else:
ok("schema.checks", "types and nullability OK")
if rows:
print(f"[ OK ] delivery.rows: {len(rows)} rows accepted")
print("-" * 70)
print("DELIVERY ACCEPTED" if verdict
else "DELIVERY REJECTED — return the report to the owner (Principle 8)")
sys.exit(0 if verdict else 1)
if __name__ == "__main__":
if len(sys.argv) != 3:
print(__doc__); sys.exit(2)
main(sys.argv[1], sys.argv[2])
A clean delivery:
[ OK ] checksum: data_1.csv [ OK ] checksum: data_2.csv [ OK ] schema.checks: types and nullability OK DELIVERY ACCEPTED
And now someone appended a row to data_2.csv after the contract was signed:
[FAIL] size: data_2.csv: 1552 bytes vs 1525 in contract [FAIL] checksum: data_2.csv: changed after signing (0627eb4c4058…) DELIVERY REJECTED — return the report to the owner (Principle 8)
Note: the schema remained correct — columns are in place, types match. A checksum catches what no schema check catches: content substitution. That's why the checksum appeared in Strengholt's descriptor earlier than in most real pipelines.
The validator checks delivery, not fact content. It won't know that billing's and ERP's "activation date" points to different events — that requires next-level quality checks.
Schema and integrity are the technical level of validation. Beyond them lie functional quality metrics: completeness, uniqueness, consistency. Strengholt recommends Deequ (AWS) and Delta Lake — a Spark framework that runs hundreds of millions of rows in minutes. Here's a PyDeequ check set matching the contract's thresholds:
from pyspark.sql import SparkSession
from pydeequ.checks import Check, CheckLevel
from pydeequ.verification import VerificationSuite
spark = SparkSession.builder.appName("quality-gate").getOrCreate()
df = spark.read.parquet("crm_customer_data/")
check = Check(spark, CheckLevel.Error, "contract-1.2.0")
result = VerificationSuite(spark) \
.onData(df) \
.addCheck(
check.hasSize(lambda x: x > 0) # delivery not empty
.isComplete("customer_id") # required
.isUnique("customer_id") # uniqueness_keys
.hasCompleteness("first_name", lambda x: x >= 0.98) # completeness_min
.isContainedIn("status", ["active", "frozen", "closed"])
) \
.run()
result_df = VerificationSuite(spark).onData(df).run()
check_result_as_dataframe(spark, result_df)
An AI assistant is appropriate here — but with a caveat that cannot be dropped. The prompt that generates a draft check suite from a domain description works like this:
"You are a data quality engineer. Here is the domain description: [description]. Field contract: [schema]. Generate: (1) Deequ checks in Scala/PyDeequ — uniqueness for keys, completeness for required fields, referential integrity for relationships; (2) dbt tests from the same set; (3) for each check — the reason why it's needed."
What's generated is a draft, not a finished control. An LLM will confidently write you a uniqueness check on first_name — technically correct but practically broken, because many people share the same first name. Without owner review, you get neatly packaged garbage with high confidence.
The right order: generation → owner review (Principle 5 — they are the owner of these facts) → inclusion in the contract.
Separately worth mentioning is the cheapest level of checks, often skipped: cross-checking metadata against each other. In Chapter 7 Strengholt directly recommends: "for checking overall metadata integrity, data management should use the same data quality tooling as it does for profiling any other data." In plain terms: the golden source registry is also data, and it breaks just like your mart breaks. If the registry has two owners for one dataset or a dataset tied to a non-existent application — that should fail CI, not wait for an annual inventory.
Principle 4 requires centralized registration, but what does that mean in practice? Strengholt has specific components, and they outlive any stack:
An autonomous repository holding general information about all unique data and its ownership. Important detail: registration is at the level of logical datasets, because relationships between datasets and applications are many-to-many: one application may have several data owners, and several physical datasets may originate from one application.
A central store of all applications and their owners (in the book — down to ServiceNow): who is deployed, where it's sold, what its confidentiality, integrity and availability ratings are. This is the second edge of the link: a data element points to an application, an application to an owner.
From Chapter 9: when an object becomes an MDM subject, master identifiers are returned to the source systems and stored there; the alternative is a lookup table that describes which local data belongs to which master data and is published to MDM. Without this table, every reconciliation is manual archaeology.
The minimum viable registry for an organization just starting: a JSON file in a git repository with a list of datasets (owner, application, contract, classification), integrity checks in CI, and a change procedure via pull request. This is fundamentally simpler than monster catalogs and, importantly, honestly reflects Principle 5: a change of owner is an explicit, discussed decision, not a silent edit in a catalog UI.
Back to Olga from the first paragraph. With the registry, her story is solved in fifteen minutes: searching for the phone_work element shows that the crm_customer_data golden dataset belongs to a CRM owner, the delivery is checksum-verified, and the marketing mart received data under Principle 6 — with the owner's approval. The edit "bypassing the API" is visible in origin — this is a violation of Principle 1, and it's fixed in the source, not in the copy.
Everything said directly affects RAG and LLM pipelines. Retrieval that indexes "something resembling the golden dataset" — an export of a copy of a copy — produces silent hallucinations: the model confidently answers by data that has already diverged from the primary source.
Let's take the same Olga story. A support agent receives the question: "what's the customer's emergency contact phone?" The index has three fragments: from the marketing mart (old number), from the HR export (no number), from the CRM golden dataset (current). Relevance ranking doesn't know that one fragment is "cleaner" — it sorts by textual match with the query. The model will, with a probability depending on phrasing, pick the old number — and answer confidently, without any caveats.
The rule from this is simple and formulated through the same model: index only golden datasets or data with a contractual guarantee of freshness and provenance — and keep golden_source and contract_version in the chunk metadata, so you can answer the question "which version of facts does this answer reference."
golden_source_id, dataset_id (from LoGS), contract_version, classification, refresh_at. Five fields — and the question "what does your answer reference" stops being philosophy.
A checklist diagnoses a breakdown; managing change requires metrics that don't age with the stack:
What percentage of datasets in LoGS have an assigned owner and a live contact. Target for the first six months — 80%; if not reached, the problem is in the registration process, not the tools.
Transfers of golden datasets without the owner's approval. Each detection is also a provenance question: how did the transfer go unnoticed?
From detection ("reports show different totals") to fixing it in the golden source. If it grows — provenance isn't working or the owner can't be found.
Validator, Deequ, dbt tests — versus eyeball verification. This is the only one of the four metrics that requires almost no cultural effort — only automation.
These metrics are deliberately modest. Strengholt doesn't promise the architecture will be built in a quarter; his book is about not building yet another slowly disintegrating centralized monolithic copy, but growing a distributed system in which every copy is responsible for its own existence.
Diagnostics "you're broken if…" — now with a remedy for each item:
distribution_approved flag in the contract and a validator check when entering a new context.
deprecation_after date in the channel metadata.
Each item is a direct consequence of one of the seven principles. If you've gone through the checklist with four or more "yes" answers — your data architecture is no longer scaling, no matter how many tools you buy.
This article is the first of three built on the book. The next one covers the decision tree for three integration architectures (RDS / API / streaming): when to use which channel, what anti-patterns to expect, and how to automate the choice. The third is on metadata as glue: catalog, provenance, self-service.
Strengholt's material is about how data architecture starts not with tools but with agreements about ownership. A golden source is not about technology — it's about every fact having a birthplace and an owner responsible for its changes. The contract is the only part of this model that can be checked by a program, and it's the easiest place to start: a 100-line validator catches content substitution that no schema check catches.
The 2026 extensions — PyDeequ, dbt tests, JSON Schema contracts, RAG chunk metadata — don't overturn the principles, they give them a working interface. The main takeaway: investments in ownership agreements pay off faster than investments in yet another catalog. A catalog without owners is a shop window without merchandise.
Sources: 1. Piethein Strengholt, «Data Management at Scale», O'Reilly Media, 2020 (Russian translation: «Масштабируемые данные», Питер, 2022) — Chapters 2, 3, 6, 7, 9. Principles 1–8, delivery contract, CSVW descriptor, RDS immutability, List of Golden Sources, golden vs. integrated data distinction, master identifier mapping tables. 2. AWS Labs, Deequ — open-source data quality framework. 3. Delta Lake, schema and quality validation — recommended in the book for delivery validation. 4. dbt Labs, model and column tests — the modern declarative-check standard the book couldn't have known about.