Schema validation pada chapter sebelumnya menegakkan bentuk tabel — tetapi siapa yang berhak menentukan bentuk tersebut, dan apa konsekuensinya bila janji itu dilanggar? Tanpa jawaban tertulis, schema hanya kebiasaan yang diingat sebagian orang: produsen data mengubah kolom seenaknya, konsumen mengetahuinya dari laporan yang meleset. Data contract adalah kesepakatan formal antara produsen dan konsumen data yang memuat schema, aturan kualitas, janji freshness, dan nama pemilik yang bisa dihubungi — lengkap dengan mekanisme penegakan otomatis. Chapter ini menyusun kontrak TokoKita sebagai berkas YAML dan membangun pemeriksanya.
Kontrak antara Produsen dan Konsumen #
Ide dasarnya sama dengan API contract di dunia layanan web: aplikasi kasir (produsen) berjanji mengirim tabel pesanan dengan bentuk dan mutu tertentu; tim analytics dan tim keuangan (konsumen) berhak menuntut janji itu dipenuhi. Bila produsen perlu mengubah kontrak — menambah kolom, memperketat SLA — perubahan diulas bersama dan versi kontrak dinaikkan, persis seperti evolusi schema pada chapter Schema Evolution yang sudah kita pelajari.
Manfaat terbesarnya adalah perpindahan titik deteksi: pelanggaran tertangkap di sisi produsen saat data keluar, bukan ditemukan konsumen berhari-hari kemudian di dashboard. Kontrak juga memberi jawaban administratif yang selama ini mengambang: siapa yang dipanggil saat data bermasalah, dan berapa lama keterlambatan yang masih ditoleransi.
Komponen Sebuah Data Contract #
| Komponen | Isi | Contoh pada Kontrak Pesanan |
|---|---|---|
| Identitas | nama, versi, pemilik, konsumen | pesanan_harian v1, pemilik tim kasir |
| Schema | kolom, tipe, wajib/opsional, unik | total integer wajib, 0-50 juta |
| Aturan kualitas | rentang nilai, daftar nilai sah | status salah satu dari empat status |
| Freshness | SLA umur data maksimal | data terbaru maksimal 24 jam |
| Evolusi | cara perubahan kontrak diusulkan | naik versi + pemberitahuan konsumen |
Komponen identitas sering dianggap pelengkap, padahal justru komponen paling sering dipakai: saat pemeriksa kontrak menemukan pelanggaran, baris pertama laporan adalah “hubungi pemilik tim kasir”.
Kontrak sebagai Berkas YAML #
Kontrak kita tulis sebagai berkas YAML — mudah dibaca manusia, mudah dibaca mesin, dan bisa disimpan di repositori yang sama dengan kode pipeline:
nama: pesanan_harian
versi: 1
pemilik: tim-kasir
konsumen: [tim-analytics, tim-keuangan]
sla_freshness_jam: 24
kolom:
order_id:
tipe: string
wajib: true
unik: true
pelanggan:
tipe: string
wajib: true
total:
tipe: integer
wajib: true
min: 0
maks: 50000000
status:
tipe: string
wajib: true
nilai_diizinkan: [baru, dibayar, kirim, selesai]
dibuat_pada:
tipe: timestamp
wajib: trueSetiap klausul pada tabel komponen kini punya rumah yang jelas: identitas di bagian atas, aturan kualitas menempel pada kolomnya masing-masing, dan SLA freshness berdiri sebagai klausul tingkat tabel. Perhatikan bahwa kontrak sengaja tidak memuat logika pemeriksaan — kontrak menyatakan apa yang dijanjikan, pemeriksa mengeksekusi bagaimana mengeceknya.
Pemeriksa Kontrak di Praktik #
Pemeriksa membaca kontrak dengan yaml.safe_load, lalu menguji dua berkas: data_lulus.csv berisi tiga baris sehat, dan data_gagal.csv berikut yang melanggar hampir semua klausul:
order_id,pelanggan,total,status,dibuat_pada
ORD-501,Budi,150000,baru,2026-10-18 21:30
ORD-501,Siti,-5000,refund,2026-10-18 20:10
,Dewi,99000,kirim,2026-10-18 18:45
ORD-504,Budi,dua ratus ribu,selesai,2026-10-18 15:20"""Pemeriksa data contract: baca kontrak YAML, periksa CSV terhadap klausulnya."""
import datetime as dt
import pandas as pd
import yaml
SEKARANG = dt.datetime(2026, 10, 20, 9, 0)
def periksa_berkas(kontrak, path):
"""Kembalikan daftar pelanggaran klausul kontrak untuk satu berkas CSV."""
df = pd.read_csv(path)
galat = []
for nama, aturan in kontrak["kolom"].items():
if nama not in df.columns:
if aturan.get("wajib"):
galat.append(f"[blokir] kolom '{nama}' wajib ada")
continue
seri = df[nama]
if aturan["tipe"] == "timestamp":
seri = pd.to_datetime(seri, errors="coerce")
if aturan.get("wajib") and seri.isna().any():
galat.append(f"[blokir] '{nama}': {int(seri.isna().sum())} baris kosong")
if aturan.get("unik") and seri.duplicated().any():
galat.append(f"[blokir] '{nama}': {int(seri.duplicated().sum())} nilai ganda")
if "min" in aturan or "maks" in aturan:
angka = pd.to_numeric(seri, errors="coerce")
bukan_angka = angka.isna() & seri.notna()
if bukan_angka.any():
galat.append(f"[blokir] '{nama}': {int(bukan_angka.sum())} nilai bukan angka")
di_luar = ~angka.between(aturan.get("min", float("-inf")), aturan.get("maks", float("inf")))
if (di_luar & angka.notna()).any():
galat.append(f"[blokir] '{nama}': {int((di_luar & angka.notna()).sum())} nilai di luar rentang")
if "nilai_diizinkan" in aturan:
di_luar = ~seri.isin(aturan["nilai_diizinkan"]) & seri.notna()
if di_luar.any():
galat.append(f"[peringatan] '{nama}': {int(di_luar.sum())} nilai di luar daftar")
kolom_waktu = next((n for n, a in kontrak["kolom"].items()
if a["tipe"] == "timestamp" and n in df.columns), None)
if kolom_waktu:
terbaru = pd.to_datetime(df[kolom_waktu], errors="coerce").max()
umur_jam = (SEKARANG - terbaru.to_pydatetime()).total_seconds() / 3600
if umur_jam > kontrak["sla_freshness_jam"]:
galat.append(f"[blokir] freshness: {umur_jam:.1f} jam melebihi SLA {kontrak['sla_freshness_jam']} jam")
return galat
with open("kontrak_pesanan.yaml", encoding="utf-8") as f:
kontrak = yaml.safe_load(f)
for berkas in ["data_lulus.csv", "data_gagal.csv"]:
galat = periksa_berkas(kontrak, berkas)
print(f"== {berkas} ==")
if not galat:
print(" LULUS: semua klausul kontrak terpenuhi")
else:
for g in galat:
print(" ", g)Output yang Dihasilkan:
== data_lulus.csv ==
LULUS: semua klausul kontrak terpenuhi
== data_gagal.csv ==
[blokir] 'order_id': 1 baris kosong
[blokir] 'order_id': 1 nilai ganda
[blokir] 'total': 1 nilai bukan angka
[blokir] 'total': 1 nilai di luar rentang
[peringatan] 'status': 1 nilai di luar daftar
[blokir] freshness: 35.5 jam melebihi SLA 24 jamMari kita telaah baris-baris kode di atas:
- Baris 48-49 (
yaml.safe_load): kontrak dimuat sebagai kamus Python;safe_loaddipilih bukanyaml.loadkarena hanya mengevaluasi tipe data dasar dan menolak objek Python arbitrer dari berkas yang tidak dipercaya. - Baris 14-18 (kolom wajib): klausul schema diperiksa pertama — kolom yang hilang membuat klausul lain kolom tersebut tidak bermakna, sehingga perulangan melanjutkan ke kolom berikutnya.
- Baris 22-25 (nullability dan uniqueness): dua aturan kualitas generik yang berlaku pada kolom mana pun asalkan klausulnya ada di kontrak.
- Baris 26-33 (angka): pemeriksaan rentang memakai
to_numeric(errors="coerce")sehingga teks"dua ratus ribu"dan angka-5000tercatat sebagai dua jenis pelanggaran yang berbeda. - Baris 38-44 (freshness): klausul SLA tingkat tabel diambil dari bagian atas kontrak, bukan dari aturan kolom — menandai bahwa freshness adalah janji tentang tabel, bukan tentang satu kolom.
- Baris 51-58 (laporan): satu pemeriksa yang sama menghasilkan vonis LULUS atau daftar pelanggaran, siap dikirim sebagai notifikasi atau dijalankan sebagai gerbang CI.
Enam pelanggaran pada data_gagal.csv memetakan sempurna ke klausul kontrak: dua pelanggaran schema (kolom kosong dan ganda), tiga pelanggaran kualitas, dan satu pelanggaran freshness. Inilah perbedaan mendasar kontrak dari validasi biasa — laporannya dapat dibaca sebagai daftar janji yang dilanggar, dan setiap janji punya nama pemilik di baris atas kontrak.
Kontrak dalam Ekosistem Tool #
Pola kontrak ini sudah diadopsi tool di sekitar kita: dbt mendukung model contracts yang menggagalkan build saat output model menyimpang dari kontraknya (dokumentasi di docs.getdbt.com ), Great Expectations menyimpan kumpulan aturan sebagai suite yang bisa dibagikan antar tim, dan Confluent Schema Registry menegakkan kompatibilitas schema pada topik Kafka sebelum pesan baru diterima.
Di antara semua klausul kontrak, freshness adalah yang paling sering dilanggar dan paling cepat terasa oleh konsumen. Chapter berikutnya membedahnya tuntas: Data Freshness.