Hijrah Backend (4): Kode Lama sebagai Executable Spec

10 September 2026 oleh Faiq Najib Al-Aziz


Kalau boleh merangkum satu aturan yang memegang kendali penuh atas migrasi ini, kualimatkan begini: yang benar adalah yang lama.1

Kedengarannya menyebalkan, saya tahu. Kamu dipercaya memindahkan sistem ke bahasa baru, dan aturan nomor satu-nya justru: jangan berubah apa-apa. Bukan lebih cepat. Bukan lebih bersih. Bukan lebih baik. Identik. Tulisan ini bedah kenapa aturan yang anti-klimaks itu justru keputusan paling masuk akal yang pernah saya jalani hehe~

Kenapa Sistem Baru Harus Setia pada Sistem Lama #

Alasannya bukan nostalgia. Ada tiga alasan yang sangat pragmatis.

Pertama, kepercayaan. Puluhan ribu user sudah bertahun-tahun percaya pada output sistem lama, angka yang sama, format yang sama, quirk yang sama. Migrasi yang diam-diam mengubah hasil adalah migrasi yang mengikis kepercayaan itu tanpa izin.

Kedua, permukaan debugging. Kalau output sistem baru berbeda dari yang lama, tersangkanya cuma satu: proses porting-nya. Logikanya sudah teruji bertahun-tahun di produksi. Tapi kalau saya sekalian “memperbaiki” logika sambil berpindah bahasa, satu bug baru berarti dua tersangka: logika barunya, atau penerjemahannya. Episode kedua sudah membuktikan betapa mahalnya debugging dengan tersangka yang salah.

Ketiga, dan ini yang paling sering dilupakan: dokumentasi yang bohong, kode yang jujur. Sistem yang saya terima nyaris tanpa dokumentasi, semua pengetahuan ada di kepala satu orang yang kalau sedang tidak ada, aplikasi eksisting itu seperti kota tua tanpa peta. Kode lama adalah satu-satunya spesifikasi yang bisa dijalankan. Spesifikasi yang bisa dijalankan tidak bisa bohong; dia selalu mengaku apa yang benar-benar dia lakukan, termasuk aibnya.

Tinggal satu catatan soal spesifikasi ini: dia jujur, tapi kadang berbicara dalam bahasa misterius. Sistem lama hobi mengulang query yang sama dalam loop, padahal sebenarnya bisa digabung jadi satu. Lalu ada sulap khas framework: tulis $this->device(), dan tiba-tiba muncul method yang entah didefinisikan di mana, entah sejak kapan, dan, yang paling bikin bengong, sukses mengembalikan response data. Di PHP, hal seperti itu dianggap wajar. Di Go, semua yang tadi tersulap itu harus kutulis eksplisit satu per satu. Awalnya terasa seperti kehilangan jalan pintas; lama-lama terasa seperti akhirnya tahu isi dapur hehe.

Aturan Main: Mana yang Setia, Mana yang Bebas #

Parity bukan berarti semua harus kembar. Yang saya pisahkan:

Harus identik: business logic, bentuk response sampai ke nama field dan tipe edge case-nya, aturan pembulatan, default aneh, dan ya, quirk.

Bebas berbeda: cara mengambil datanya. Contoh favorit saya sudah diceritakan di episode kedua: endpoint yang di PHP menjalankan ratusan query kecil bertingkat, di Go diganti satu query CTE. Hasil akhirnya wajib sama, jalan menuju sana bebas.

Perbedaan ini yang membuat migrasi tetap menarik: kamu tidak sedang menyalin kamus, kamu sedang membangun jembatan baru dengan alun-alun yang sama persis di kedua ujungnya.

Cerita: “Perbaikan” yang Harus Saya Hapus Sendiri #

Ini bagian favorit saya, karena jadi pelajaran mahal.

Ada satu endpoint riwayat perjalanan kendaraan yang responsnya di sistem lama berisi beberapa field tambahan: riwayat alarm, aktivitas geofence, dan insiden perilaku mengemudi. Versi Go awal saya mengisi ketiganya dengan semestinya, alarm speed limit, SOS, masuk/keluar geofence. Lebih kaya, lebih berguna. Saya merasa keren sebentar.

Lalu saya telusuri sistem lamanya, dan ketemu pemandangan yang sekarang jadi gambar favorit saya:2

//get data alarm history
// $history_alarms = $device->getHistoryAlarm($start_date, $end_date, ...);
$history_alarms = [];

Query aslinya dikomentari orang. Tidak dihapus, dikomentari, lalu diganti array kosong. Tiga field itu ternyata sudah bertahun-tahun selalu berisi kosong di sistem produksi. Dan setelah saya cek ke frontend: tidak satu pun yang dipakai.

Keputusannya kalimat yang aneh untuk diucapkan seorang engineer: “hapus perbaikan kita, samakan dengan yang lama: kosongkan.”

Dan rasanya memang aneh. Sengaja menurunkan “kualitas” kode sendiri supaya setia pada sistem yang sedang kamu gantikan. Tapi itulah poinnya: perbaikan diam-diam adalah racun dalam migrasi. Kalau suatu hari memang perlu mengisi data alarm itu, dia harus jadi keputusan eksplisit yang terpisah, bukan kejutan yang lolos bareng migrasi. Perbaikan boleh; kejutan tidak boleh hehe~

Quirk Kecil yang Wajib Ditiru #

Parity juga turun sampai hal yang kelihatannya remeh. Contoh nyata: kalau total waktu bergerak sebuah kendaraan nol, sistem lama menampilkan tanda strip '-', bukan 0 menit. Alasannya sederhana: helper di kode lama cuma dipanggil kalau nilainya positif, jadi nol tidak pernah kebagian format. Versi awal saya menampilkan 0 menit, secara teknis lebih benar, secara produksi: beda, berarti salah.

Plus satu edge case: rentang tanggal yang tidak wajar (misalnya tanggal akhir di masa depan). Sistem lama membalas envelope kosong berformat khusus; versi awal saya membalas array polos. User tidak akan pernah tahu bedanya, sampai suatu hari script mereka yang mem-parse response tahu hahaha.

Meniru perilaku aneh secara sadar, lalu menuliskannya di test dengan muka datar, itu pengalaman yang unik. Kamu tahu itu bukan “cara yang benar”. Kamu tahu itu “cara yang sudah dipercaya”. Dan dalam migrasi, yang kedua menang.

Pajak Paritas: Gelondongan Field #

Kebiasaan warisan lain: response JSON di sistem lama cenderung dikirim satu gelondong. Satu endpoint membalas dengan puluhan field, sebagian bersarang dalam-dalam, dan tidak semuanya dipakai frontend-nya. Di PHP, biaya menyerahkan gelondongan itu terasa ringan. Di Go, yang justru terkenal efisien, menyerialisasi gelondongan itu jadi beban nyata yang dibayar di tiap request. Parity memaksa saya membawanya dulu apa adanya; itu harga setianya hehe.

Gelondongan itu juga lahir dari kebiasaan yang menular: frontend tidak konsisten dari endpoint mana dia mengambil sebuah field, kadang dari sini, kadang dari sana, tergantung selera masanya. Konvensi penamaan pun ikut berganti-ganti antar era. Hasil akhirnya: aplikasi eksisting terasa seperti rumah tambal sulam. Tapi jujur, untuk ukuran tambal sulam, dia kokoh juga, udah bertahun-tahun dipakai puluhan ribu orang hahaha.

Konsistensinya memang tidak berhenti di sumber data. Tipe datanya pun ikut berimprovisasi: field yang sama kadang dikirim sebagai angka, kadang, saat kosong, sebagai string kosong. Ke PHP, perbedaan secuil itu terlalu sopan untuk digubris. Ke Go yang strict typing, string kosong yang mengaku angka itu berdiri di depan pintu sambil menolak mengisi formulir hahaha.

Lebih kreatifnya lagi: field yang sama, dari halaman berbeda ke endpoint serupa, sekali dikirim sebagai angka, sekali sebagai string. PHP menerima keduanya sambil senyum. Go menolak keduanya sambil menyerahkan error decoding yang panjang hahaha.

Satu lagi yang hampir jadi candaan sesama developer: sistem lama gemar membalas hasil kosong dengan HTTP 200 berisi [] atau null. Padahal bisa saja dikirim kode status yang jujur, sekalian menjelaskan bahwa datanya tidak ada. Tapi frontend sudah bertahun-tahun membangun hidupnya di atas [] itu, dan aturan parity bilang: tiru. Jadi sistem Go saya pun ikut melapor “semua aman” sambil membawa kabar kosong hahaha.

Karena tahu respons saya nanti gelondongan, saya membangun alat buktinya: audit kecil yang memeriksa frontend untuk tiap field, mana yang benar-benar dibaca, mana yang cuma numpang lewat. Audit itulah yang jadi kuitansi saat giliran membereskan tiba. Dia juga yang membantu saya saat harus mengosongkan field riwayat alarm di cerita di atas: tidak ada satu pun yang membacanya.

Parity Bukan Dogma #

Sampai di sini bisa terdengar seperti sekte kesetiaan. Tenang, parity punya pengecualian.

Ada satu penyesuaian timezone di sistem lama yang jelas-jelas bug, menggeser data mundur satu-dua jam di timezone tertentu. Yang itu saya tidak tiru. Bedanya dengan kasus alarm: pengecualian ini saya ambil secara sadar, saya catat alasannya, dan saya siap mempertanggungjawabkannya. Parity adalah default, bukan dogma. Pengecualiannya boleh ada, asal pengecualiannya tahu diri: sadar, tercatat, dan bisa dijelaskan.

Sisi Lain Spesifikasi: Soal Dependensi #

Di bulan-bulan awal, saya menerima satu saran dari senior yang memimpin proyek ini: “sebisa mungkin pakai standar library saja.”

Reaksi internal saya waktu itu: skeptis. Bagi saya aturan semacam itu terlalu kaku, yang penting kita sadar library yang kita pakai rawan breaking changes atau tidak, kan? Tapi saya mengiyakan. Alasannya sederhana dan sedikit memalukan: saya takut kalau repo saya di-inspeksi dan saya tidak mengikuti instruksi, saya kena tegur hehe.

Lucunya: sampai proyek berjalan berbulan-bulan, inspeksi itu tidak pernah datang. Dan kenyataan akhirnya berbicara sendiri, sampai tulisan ini dibuat, go.mod saya berisi 22 dependensi langsung: web framework, driver database, cache, structured logging, config, dan teman-temannya.

Filosofi yang akhirnya benar-benar saya jalani bukan “standar library saja”, tapi lebih longgar dan justru lebih menuntut: setiap dependensi harus bisa menjawab satu pertanyaan, kenapa standar library tidak cukup untuk ini? Framework HTTP? Standar library memang belum nyaman. Date/time handling? Standar library cukup, jadi tidak ada library tambahan. Aturannya bukan soal berapa banyak, tapi soal tiap baris punya alasan.

Pelajaran kecilnya: aturan yang ditegakkan dengan rasa takut akan ditaati, tapi aturan yang ditaati tanpa dipahami tidak akan bertahan. Untungnya yang terakhir terjadi ke saya, dan hasilnya lebih sehat dari aturannya sendiri hahaha.

Pelajaran #

  1. Spesifikasi terbaik untuk migrasi adalah kode lama itu sendiri. Dia selalu up-to-date, tidak pernah bohong, dan bisa dijalankan sebagai pembanding.
  2. Kesetiaan lebih penting daripada keunggulan. Perbaikan boleh datang, tapi setelah sistem baru stabil, sebagai keputusan eksplisit yang terpisah.
  3. Pengecualian parity harus sadar dan tercatat. Default-nya tiru; yang tidak ditiru harus bisa dijelaskan kenapa.
  4. Aturan dependensi yang baik berisi alasan, bukan angka. “Standar library saja” terdengar disiplin; “tiap dependensi wajib punya alasan” itu yang benar-benar bisa dijalankan.

Episode berikutnya kita ke dapur database: kenapa query yang sudah punya index bagus bisa memilih untuk tidak memakainya, dan kenapa pembungkus sekecil DATE() bisa menghukum tabel puluhan juta baris. Sampai ketemu di sana.

Sekian. Salam.


  1. “Yang benar adalah yang lama” berlaku selama masa migrasi. Setelah sistem baru stabil dan terverifikasi, statusnya lulus uji dan mulai berhak menentukan sendiri arah perubahannya. ↩︎

  2. Potongan kode dalam tulisan ini disederhanakan dari bentuk aslinya, nama field dan struktur dipertahankan secukupnya untuk bercerita, detail internal diubah. ↩︎