Schema yang baik hari ini tidak akan tetap baik selamanya. Bisnis menambah kolom diskon, regulasi meminta kolom pajak, dan definisi lama berubah seiring produk bertumbuh. Masalahnya, data lama yang sudah tertulis dengan schema lama tidak bisa diubah massal — mengubah 5 tahun arsip transaksi hanya demi menambah satu kolom terlalu mahal. Schema evolution adalah disiplin mengubah schema tanpa memutus pembaca data lama. Avro adalah laboratorium terbaik untuk mempelajarinya karena mekanisme resolusi schema-nya terdokumentasi rapi di dokumentasi resmi Avro .

Tiga Jenis Kompatibilitas #

Istilah kunci di dunia schema evolution adalah kompatibilitas, yaitu hubungan antara schema penulis (writer schema) dan schema pembaca (reader schema):

JenisArtiSyarat umum
Backwardschema baru bisa membaca data lamafield baru wajib punya nilai default
Forwardschema lama bisa membaca data barufield lama punya default atau diabaikan pembaca lama
Fullkeduanya sekaligusperubahan hanya penambahan field ber-default

Kebijakan yang paling umum di industri adalah backward-first: schema registry seperti Confluent Schema Registry secara bawaan menolak schema baru yang tidak backward compatible terhadap versi sebelumnya. Alasannya praktis — konsumen data sering kali diperbarui lebih lambat daripada produsennya, sehingga data lama harus tetap terbaca oleh pipeline lama.

Aturan Evolusi yang Aman #

Enam aturan berikut menjaga kompatibilitas pada mayoritas format ber-schema:

  • Tambah field baru selalu dengan nilai default.
  • Jangan hapus field yang masih dipakai; tandai usang (deprecation) lebih dulu.
  • Jangan ubah tipe field menjadi tipe yang tidak kompatibel (int ke string).
  • Jangan pindahkan field ke struktur lain sebelum konsumen bermigrasi.
  • Jangan gunakan ulang penanda lama — nomor field Protobuf atau nama field Avro.
  • Untuk penggantian nama di Avro, gunakan aliases agar nama lama tetap terpetakan.

Aturan-aturan ini terasa abstrak sampai kita lihat mesin resolusi bekerja. Kita buktikan dengan Avro dan pustaka fastavro.

Membaca File Lama dengan Schema Baru #

Skenarionya realistis: arsip transaksi ditulis awal tahun dengan schema v1 tanpa kolom diskon. Bulan ini aplikasi kasir memakai schema v2 yang menambah diskon dengan default 0. Kode berikut menulis file v1, lalu membacanya dengan v2 sebagai reader schema — dan sebaliknya:

python
import io
from fastavro import writer, reader

SKEMA_V1 = {
    "type": "record",
    "name": "Transaksi",
    "fields": [
        {"name": "id", "type": "int"},
        {"name": "kasir", "type": "string"},
        {"name": "produk", "type": "string"},
        {"name": "qty", "type": "int"},
        {"name": "harga", "type": "int"},
    ],
}

SKEMA_V2 = {
    "type": "record",
    "name": "Transaksi",
    "fields": [
        {"name": "id", "type": "int"},
        {"name": "kasir", "type": "string"},
        {"name": "produk", "type": "string"},
        {"name": "qty", "type": "int"},
        {"name": "harga", "type": "int"},
        {"name": "diskon", "type": "int", "default": 0},
    ],
}

catatan_v1 = [
    {"id": 1, "kasir": "Budi", "produk": "Kopi Robusta", "qty": 2, "harga": 45000},
    {"id": 2, "kasir": "Siti", "produk": "Teh Melati", "qty": 3, "harga": 15000},
]
catatan_v2 = [dict(c, diskon=5000) for c in catatan_v1]

# Tulis file dengan schema v1
buffer = io.BytesIO()
writer(buffer, SKEMA_V1, catatan_v1)
with open("transaksi_v1.avro", "wb") as f:
    f.write(buffer.getvalue())

# Backward: file v1 dibaca dengan schema v2 (reader schema)
with open("transaksi_v1.avro", "rb") as f:
    for catatan in reader(f, reader_schema=SKEMA_V2):
        print(catatan)

# Forward: file v2 dibaca dengan schema v1
buffer = io.BytesIO()
writer(buffer, SKEMA_V2, catatan_v2)
with open("transaksi_v2.avro", "wb") as f:
    f.write(buffer.getvalue())
with open("transaksi_v2.avro", "rb") as f:
    for catatan in reader(f, reader_schema=SKEMA_V1):
        print(catatan)

Output yang Dihasilkan:

text
{'id': 1, 'kasir': 'Budi', 'produk': 'Kopi Robusta', 'qty': 2, 'harga': 45000, 'diskon': 0}
{'id': 2, 'kasir': 'Siti', 'produk': 'Teh Melati', 'qty': 3, 'harga': 15000, 'diskon': 0}
{'id': 1, 'kasir': 'Budi', 'produk': 'Kopi Robusta', 'qty': 2, 'harga': 45000}
{'id': 2, 'kasir': 'Siti', 'produk': 'Teh Melati', 'qty': 3, 'harga': 15000}

Mari kita telaah baris-baris kode di atas:

  1. Baris 16-27 (SKEMA_V2): schema baru menambah field diskon bertipe int dengan "default": 0 — kunci dari seluruh mekanisme.
  2. Baris 35-39 (writer(buffer, SKEMA_V1, ...)): menulis file Avro; writer schema tertanam di header file, sehingga file tahu dirinya ditulis dengan v1.
  3. Baris 41-44 (reader(..., reader_schema=SKEMA_V2)): pembaca memakai v2 sebagai lensa. Karena data tidak punya diskon, resolusi schema mengisi nilai default 0 — dua baris pertama output membuktikannya.
  4. Baris 46-53 (file v2 dibaca dengan v1): arah sebaliknya. Field diskon=5000 ada di data tetapi tidak dikenali schema v1, sehingga resolusi mengabaikannya — dua baris terakhir output tidak memuat diskon.

Dua arah ini adalah bukti backward dan forward compatibility bekerja. Penambahan field ber-default memberi kompatibilitas full pada kasus ini.

Field Baru Tanpa Default #

Sekarang kita sengaja melanggar aturan: schema baru menambah diskon tanpa default. Data lama yang sedang dibaca tidak memuat nilai pengganti, dan resolusi gagal:

python
import io
from fastavro import writer, reader

SKEMA_V1 = {
    "type": "record", "name": "Transaksi",
    "fields": [
        {"name": "id", "type": "int"},
        {"name": "kasir", "type": "string"},
        {"name": "produk", "type": "string"},
        {"name": "qty", "type": "int"},
        {"name": "harga", "type": "int"},
    ],
}
SKEMA_TANPA_DEFAULT = {
    "type": "record", "name": "Transaksi",
    "fields": SKEMA_V1["fields"] + [{"name": "diskon", "type": "int"}],
}

buffer = io.BytesIO()
writer(buffer, SKEMA_V1, [
    {"id": 1, "kasir": "Budi", "produk": "Kopi Robusta", "qty": 2, "harga": 45000}
])

buffer.seek(0)
try:
    list(reader(buffer, reader_schema=SKEMA_TANPA_DEFAULT))
except Exception as galat:
    print(type(galat).__name__ + ":", galat)

Output yang Dihasilkan:

text
SchemaResolutionError: No default value for field diskon in Transaksi

Dampak ke Pipeline Downstream #

Schema evolution bukan urusan satu file — pipeline bercabang, dan setiap cabang punya kecepatan pembaruan sendiri. Praktik yang sehat di industri: pusatkan schema di registry (Confluent Schema Registry untuk Kafka adalah contoh paling umum, lihat dokumentasinya ), aktifkan pemeriksaan kompatibilitas otomatis, lalu naikkan versi schema secara bertahap — produsen baru, konsumen menyesuaikan, baru field lama ditandai usang. Orchestration tool seperti Airflow membantu mengurutkan tahapan ini sehingga tidak ada pekerjaan membaca schema yang belum siap.

Dengan schema yang aman berevolusi, satu pertimbangan besar masih tersisa di setiap keputusan format: ukuran dan kecepatan. Chapter berikutnya membahas seni mengompresi data: Kompresi Data.