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:

AspekBronzeSilverGold
Padanan layeredrawstagingmart
Isievent apa adanyabersih, dedup, aturan bisnisagregat siap analitik
Metadata kunciingested_atalasan karantinadefinisi metrik bisnis
Konsumenreprocessing, audittim data, MLdashboard, 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:

python
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:

python
# --- 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:

python
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:

text
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:

  1. 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”.
  2. Blok kedua, baris 2-12 (silver_checkout): aturan bisnis jumlah >= 1 AND jumlah * harga <= 50000000 menyaring event tidak wajar, sementara window function row_number() OVER (PARTITION BY event_id ...) mendedup e-103.
  3. Blok kedua, baris 13-21 (silver_karantina): NOT EXISTS menangkap semua event Bronze yang gagal masuk Silver, dan CASE menuliskan alasannya — dua event gagal (e-105, e-107) muncul di output dengan alasan masing-masing.
  4. Blok kedua, baris 24-30 (gold_penjualan): agregat harian per kategori; satu definisi pendapatan untuk semua konsumen Gold.
  5. Blok ketiga, baris 1-7 (gerbang): setiap cek adalah SQL penghitung pelanggaran; hasil 0 berarti 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.