Pola raw-staging-mart pada chapter Layered Data Architecture menjawab pertanyaan “dari mana data berasal dan bagaimana membangunnya ulang”. Medallion architecture membawa pola yang sama ke atas lakehouse dan menambahkan disiplin yang di dunia nyata sering hilang: gerbang kualitas di setiap perpindahan medali, karantina untuk baris gagal, dan rekonsiliasi angka antar layer. Penamaan Bronze, Silver, Gold dipopulerkan Databricks dan kini menjadi kosakata baku ekosistem lakehouse. Bab ini membangun ketiga medali tersebut dengan DuckDB, lengkap dengan gerbang yang mencatat LULUS atau GAGAL.
Medallion: Layered di Atas Lakehouse #
Medallion bukan penamaan ulang kosmetik atas tiga layer. Setiap medali menambah kontrak yang lebih tegas daripada versi sebelumnya: Bronze menyimpan event apa adanya plus metadata ingestion, Silver menegakkan aturan bisnis dan schema, dan Gold menyimpan satu definisi metrik untuk semua konsumen. Prinsip reprocessability tetap sama — Gold dan Silver selalu bisa dibangun ulang dari Bronze.
Pemetaan medallion terhadap layer klasik:
| Aspek | Bronze | Silver | Gold |
|---|---|---|---|
| Padanan layered | raw | staging | mart |
| Isi | event apa adanya | bersih, dedup, aturan bisnis | agregat siap analitik |
| Metadata kunci | ingested_at | alasan karantina | definisi metrik bisnis |
| Konsumen | reprocessing, audit | tim data, ML | dashboard, laporan |
Gerbang Kualitas Antar Medali #
Pada pola layered biasa, pembersihan diam-diam membuang baris bermasalah dan tidak ada yang tahu berapa banyak yang dibuang. Medallion menuntut sebaliknya: setiap perpindahan medali melewati gerbang — daftar pemeriksaan yang hasilnya tercatat, bukan asumsi. Baris yang gagal aturan bisnis tidak dibuang, melainkan masuk tabel karantina beserta alasannya, sehingga tim domain bisa memperbaiki kualitas di akarnya.
Kita simulasikan alur checkout TokoKita. Dua belas event berikut memuat satu duplikat (e-103), satu event dengan jumlah nol, dan satu event dengan harga tidak wajar — semuanya sengaja ada untuk diuji gerbang:
import duckdb
con = duckdb.connect()
# --- Bronze: event checkout apa adanya + metadata waktu ingestion ---
con.execute("""
CREATE TABLE bronze_checkout AS
SELECT * FROM (VALUES
('2026-01-15 09:12:33', 'e-101', 'kopi', 2, 45000),
('2026-01-15 10:05:11', 'e-102', 'teh', 1, 16000),
('2026-01-15 11:40:07', 'e-103', 'alat', 1, 55000),
('2026-01-15 11:40:07', 'e-103', 'alat', 1, 55000),
('2026-01-15 13:02:49', 'e-104', 'kopi', 3, 12000),
('2026-01-15 15:44:20', 'e-105', 'teh', 0, 18000),
('2026-01-16 08:20:05', 'e-106', 'kopi', 2, 52000),
('2026-01-16 09:15:38', 'e-107', 'alat', 1, 99999999),
('2026-01-16 10:30:52', 'e-108', 'kopi', 1, 60000),
('2026-01-16 12:00:14', 'e-109', 'teh', 2, 12000),
('2026-01-16 14:45:29', 'e-110', 'kopi', 1, 60000),
('2026-01-16 18:22:41', 'e-111', 'alat', 1, 48000)
) AS t(event_time, event_id, kategori, jumlah, harga)
""")
con.execute("ALTER TABLE bronze_checkout ADD COLUMN ingested_at DATE DEFAULT '2026-01-17 02:00'")Silver kemudian mendedup event, menegakkan aturan bisnis, dan mengkarantina yang gagal; Gold mengagregasi hasil bersihnya:
# --- Silver: dedup event_id + aturan bisnis; baris gagal dikarantina ---
con.execute("""
CREATE TABLE silver_checkout AS
SELECT event_time, event_id, kategori, jumlah, harga,
jumlah * harga AS total
FROM (
SELECT *, row_number() OVER (
PARTITION BY event_id ORDER BY event_time) AS n
FROM bronze_checkout
WHERE jumlah >= 1 AND jumlah * harga <= 50000000
) WHERE n = 1
""")
con.execute("""
CREATE TABLE silver_karantina AS
SELECT b.event_id, b.kategori, b.jumlah, b.harga,
CASE WHEN b.jumlah < 1 THEN 'jumlah di bawah 1'
ELSE 'nilai transaksi di atas batas' END AS alasan
FROM bronze_checkout b
WHERE NOT EXISTS (SELECT 1 FROM silver_checkout s
WHERE s.event_id = b.event_id)
""")
# --- Gold: agregat harian siap analitik ---
con.execute("""
CREATE TABLE gold_penjualan AS
SELECT substr(event_time, 1, 10) AS tanggal, kategori,
count(*) AS jumlah_event, sum(total) AS pendapatan
FROM silver_checkout
GROUP BY 1, 2
""")Gerbang kualitas sendiri adalah fungsi kecil yang menjalankan daftar cek SQL — setiap cek wajib mengembalikan nol pelanggaran:
def gerbang(nama_layer, pemeriksaan):
"""Jalankan daftar cek; setiap cek wajib bernilai 0 agar LULUS."""
print(f"== Gerbang kualitas {nama_layer} ==")
for nama, sql in pemeriksaan:
pelanggaran = con.execute(sql).fetchone()[0]
status = "LULUS" if pelanggaran == 0 else "GAGAL"
print(f" {nama:<34} {pelanggaran:>2} pelanggaran {status}")Pemanggilan gerbang beserta pencetakan laporan dipangkas dari tampilan; hasil eksekusi skrip lengkap berikut:
Output yang Dihasilkan:
Bronze : 12 event dimuat apa adanya + kolom ingested_at
== Gerbang kualitas Bronze ==
event_id kosong 0 pelanggaran LULUS
event_time kosong 0 pelanggaran LULUS
Silver : 9 baris bersih (dedup + aturan bisnis)
karantina e-105 (teh, jumlah=0, harga=18000): jumlah di bawah 1
karantina e-107 (alat, jumlah=1, harga=99999999): nilai transaksi di atas batas
== Gerbang kualitas Silver ==
event_id duplikat 0 pelanggaran LULUS
total != jumlah x harga 0 pelanggaran LULUS
Gold : agregat harian per kategori
2026-01-15 alat 1 event Rp55,000
2026-01-15 kopi 2 event Rp126,000
2026-01-15 teh 1 event Rp16,000
2026-01-16 alat 1 event Rp48,000
2026-01-16 kopi 3 event Rp224,000
2026-01-16 teh 1 event Rp24,000
rekonsiliasi sum(silver) - sum(gold) = 0 (LULUS)Mari kita telaah baris-baris kode di atas:
- Blok pertama, baris 23 (
ALTER TABLE ... ingested_at): Bronze mencatat waktu ingestion sebagai kolom default, memisahkan “kapan event terjadi” (event_time) dari “kapan data tiba di platform”. - Blok kedua, baris 2-12 (
silver_checkout): aturan bisnisjumlah >= 1 AND jumlah * harga <= 50000000menyaring event tidak wajar, sementara window functionrow_number() OVER (PARTITION BY event_id ...)mendedupe-103. - Blok kedua, baris 13-21 (
silver_karantina):NOT EXISTSmenangkap semua event Bronze yang gagal masuk Silver, danCASEmenuliskan alasannya — dua event gagal (e-105,e-107) muncul di output dengan alasan masing-masing. - Blok kedua, baris 24-30 (
gold_penjualan): agregat harian per kategori; satu definisipendapatanuntuk semua konsumen Gold. - Blok ketiga, baris 1-7 (
gerbang): setiap cek adalah SQL penghitung pelanggaran; hasil0berarti LULUS, angka lain berarti gerbang menahan perpindahan layer.
Karantina dan Rekonsiliasi #
Perhatikan alur dua belas event pada output: dua belas masuk Bronze, sepuluh baris bersih lolos ke Silver (satu duplikat digabung, dua event lain dikarantina), dan Gold menutup dengan rekonsiliasi sum(silver) - sum(gold) = 0. Angka nol tersebut membuktikan tidak ada nilai yang hilang atau tercipta di perpindahan Silver ke Gold. Rekonsiliasi seperti ini adalah cek murah yang sebaiknya menyertai setiap gerbang.
Rujukan resmi pola medallion di ekosistem lakehouse tersedia di dokumentasi Databricks , dan contoh table format pendukungnya telah kita bahas pada chapter Delta Lake serta Apache Iceberg.
Medallion dibangun untuk pipeline batch: kesegaran data ditentukan jadwal orchestration, biasanya per jam atau per hari. Ketika kebutuhan turun ke menit atau detik — misalnya deteksi transaksi mencurigakan — kita membutuhkan kecepatan dan keakuratan sekaligus. Chapter Lambda Architecture memperlihatkan cara menjalankan dua kecepatan tersebut dalam satu arsitektur.