Hijrah Backend (4): Kode Lama sebagai Executable Spec



Kalau disuruh merangkum satu aturan yang memegang kendali penuh atas migrasi ini, jawaban saya: yang benar adalah yang lama.1

Menyebalkan, ya, saya tahu. Dipercaya memindahkan sistem ke bahasa baru, eh aturan nomor satunya malah: jangan ubah apa-apa. Bukan lebih cepat, bukan lebih bersih, bukan lebih bagus. Identik. Tapi setelah menjalaninya, saya paham kenapa aturan yang antiklimaks ini masuk akal.

Kenapa Sistem Baru Harus Setia pada Sistem Lama #

Bukan karena nostalgia. Alasannya pragmatis banget.

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 itu mengikis kepercayaan tanpa izin.

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

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

Jujur sih, tapi kadang bicaranya seperti teka-teki. Sistem lama hobi mengulang query yang sama di dalam loop, padahal bisa digabung jadi satu. Ada juga sulap khas framework: tulis $this->device(), lalu muncul method yang entah didefinisikan di mana dan sejak kapan. Yang bikin bengong, method itu berhasil mengembalikan data. Di PHP itu dianggap wajar. Di Go, semua yang tadi tersulap harus saya tulis eksplisit satu per satu. Awalnya terasa kehilangan jalan pintas, tapi lama-lama saya jadi tahu isi dapurnya.

Aturan Main: Mana yang Setia, Mana yang Bebas #

Parity bukan berarti semuanya harus kembar. Pembagiannya begini:

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

Boleh beda: 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 ke sana bebas.

Pembagian ini bikin migrasinya tetap seru. Saya tidak sedang menyalin kamus, tapi membangun jembatan baru dengan alun-alun yang persis sama di kedua ujungnya.

Cerita: “Perbaikan” yang Harus Saya Hapus Sendiri #

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 semuanya dengan benar: alarm speed limit, SOS, serta aktivitas masuk dan keluar geofence. Lebih kaya dan lebih berguna, setidaknya menurut saya waktu itu. Ehem.

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, tapi dikomentari, lalu diganti array kosong. Jadi tiga field itu ternyata sudah bertahun-tahun selalu kosong di produksi. Dan setelah saya cek ke frontend: tidak satu pun dipakai.

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

Rasanya memang aneh, sengaja menurunkan “kualitas” kode sendiri demi setia pada sistem yang sedang digantikan. Tapi perbaikan diam-diam itu racun dalam migrasi. Kalau suatu hari data alarm itu memang perlu diisi, putuskan secara terpisah, jangan diselipkan ke migrasi.

Quirk Kecil yang Wajib Ditiru #

Parity turun sampai ke 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, tapi secara produksi beda, berarti salah.

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

Meniru perilaku aneh lalu menuliskannya di test dengan muka datar itu rasanya unik juga. Saya tahu ini bukan “cara yang benar”, tapi “cara yang sudah dipercaya”. 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. Di PHP, ongkos menyerahkan gelondongan itu terasa ringan. Di Go, yang terkenal efisien, menyerialisasi gelondongan itu jadi beban nyata yang dibayar di tiap request. Parity memaksa saya membawanya dulu apa adanya. Itu harga kesetiaan hehe.

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

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

Lebih kreatif 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 menyodorkan error decoding yang panjang.

Satu lagi yang hampir jadi bahan candaan sesama developer: sistem lama gemar membalas hasil kosong dengan HTTP 200 berisi [] atau null. Kode status lain sebenarnya bisa menjelaskan bahwa datanya tidak ada, tapi frontend sudah bertahun-tahun bergantung pada [] itu. Aturan parity bilang: tiru. Jadi sistem Go saya pun ikut melapor “semua aman” sambil membawa kabar kosong.

Karena sudah tahu respons saya bakal gelondongan, saya bikin alat buktinya: audit kecil yang memeriksa frontend untuk tiap field, mana yang benar-benar dibaca, mana yang cuma numpang lewat. Audit itu jadi kuitansi saat giliran beres-beres tiba. Dia juga yang membantu saya waktu mengosongkan field riwayat alarm di cerita tadi: ternyata tidak ada yang membacanya.

Parity Bukan Dogma #

Sampai sini mungkin terdengar seperti sekte kesetiaan. Tenang, parity ada pengecualiannya.

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

Sisi Lain Spesifikasi: Soal Dependensi #

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

Reaksi dalam hati saya waktu itu: skeptis. Menurut saya aturan begitu terlalu kaku, yang penting kita sadar library yang dipakai rawan breaking changes atau tidak, kan? Tapi saya iya-iya saja. Alasannya sederhana dan agak memalukan: takut kalau repo saya diinspeksi dan saya tidak mengikuti instruksi, saya kena tegur hehe.

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

Prinsip yang akhirnya benar-benar saya jalani bukan “standar library saja”, tapi lebih longgar sekaligus 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. Intinya bukan soal jumlah, tapi tiap baris harus punya alasan.

Aturan yang ditegakkan dengan rasa takut mungkin ditaati, tapi aturan yang tidak dipahami tidak akan bertahan. Untung saya akhirnya menemukan prinsip yang lebih sehat daripada sekadar menurut.

Catatan Akhir #

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

Episode berikutnya kita ke dapur database: query ber-index yang memilih tidak memakainya, dan pembungkus DATE() yang memperlambat tabel berisi puluhan juta baris.

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. ↩︎