Titen: Aplikasi Survei yang Menganggap Sinyal Pasti Hilang

Titen: Aplikasi Survei yang Menganggap Sinyal Pasti Hilang

Daftar Isi

Seorang enumerator masuk ke sebuah desa untuk mendata usaha mikro. Di suatu titik antara jalan raya dan rumah ketiga, sinyalnya hilang. Ia terus bekerja, karena pekerjaannya tidak berhenti hanya karena satu bar sinyal. Dua jam kemudian sinyal kembali dalam perjalanan pulang, di atas kendaraan yang bergerak, lewat koneksi yang putus-nyambung setiap beberapa detik.

Semua yang sulit dari proyek ini ada di kalimat terakhir itu.

Aplikasi survei yang bisa bekerja offline itu mudah. Tulis ke database lokal, sinkronkan nanti. Kesulitannya ada pada saat sinkronisasi berlangsung, ketika koneksinya cukup buruk untuk memutus satu permintaan di tengah jalan, tapi cukup baik untuk terus mencoba lagi. Salah menangani itu dan yang terjadi bukan kehilangan data, melainkan sesuatu yang lebih buruk: data ganda yang terlihat sah, dan tidak ada yang menyadarinya sampai totalnya meleset di akhir bulan.

Maka saya membangun Titen di atas satu asumsi tunggal. Jaringan akan gagal di tengah permintaan, klien akan mencoba lagi, dan server harus baik-baik saja dengan itu.

Keputusan yang menentukan

Outbox per mutasi, bukan tombol sinkronisasi

Rancangan naifnya mengantrekan “data yang belum terkirim” lalu mendorongnya saat online. Ia runtuh begitu satu data dalam satu batch gagal: yang diulang batch-nya atau datanya, dan antreannya sekarang dalam keadaan apa?

Titen menulis setiap perubahan sebagai entri outbox tersendiri pada detik perubahan itu terjadi. Tiap entri membawa apa yang berubah, kapan, dan berapa kali sudah dicoba. Sebuah worker latar belakang menguras outbox dengan exponential backoff, satu entri pada satu waktu, dan entri hanya keluar dari outbox setelah server mengonfirmasinya.

Enumeratornya tidak pernah menekan tombol sinkronisasi. Tidak ada layar sinkronisasi. Tugas aplikasinya adalah mengosongkan antrean itu pada akhirnya, dan sementara itu jujur tentang apa yang belum mendarat.

Idempoten pada UUID yang dibuat klien

Ini bagian yang membuat pengiriman ulang jadi aman, dan ia satu keputusan desain yang menghapus satu kategori bug sekaligus.

Klien membuat UUID saat datanya dibuat, bukan saat dikirim. Setiap endpoint memperlakukan UUID itu sebagai identitas operasinya. Kirim mutasi yang sama lima kali, server membuat satu baris dan mengembalikan hasil yang sama lima kali.

Artinya klien boleh bodoh soal pengiriman ulang. Ia tidak perlu tahu apakah percobaan sebelumnya sempat sampai ke server sebelum koneksinya mati, dan itu memang pertanyaan yang secara mendasar tidak bisa ia jawab. Ia tinggal mengirim lagi.

Terukur pada 200 submission uji di sinyal yang sengaja dibuat tidak stabil: 0% kehilangan data, dan 95% sinkronisasi selesai tanpa ada yang menyentuh aplikasinya. Sisa 5% tuntas pada pembukaan aplikasi berikutnya.

Foto lewat jalur sendiri

Foto itu persoalan yang berbeda dari data formulir. Ukurannya besar, lambat, dan kehilangan satu foto lebih ringan akibatnya daripada kehilangan survei tempat foto itu menempel.

Jadi foto disinkronkan lewat jalur terpisah dengan antreannya sendiri, dikompresi cukup kuat untuk lolos di 2G. Sebuah foto baru dihapus dari perangkat setelah server mengonfirmasi ia sudah memilikinya. Artinya ponsel yang kehabisan ruang penyimpanan merosot dengan cara menolak foto baru, bukan dengan diam-diam membuang foto yang sudah diambil.

Dua binary, supaya panelnya bisa jatuh sendirian

API dan panel admin adalah dua binary terpisah di atas database yang sama. Menyajikan keduanya dari satu proses jelas lebih sedikit pekerjaannya.

Alasan untuk tidak begitu: panel admin adalah tempat kueri tanpa batas ditulis. Seseorang memfilter dashboard pada rentang tanggal yang tidak pernah diuji, lalu prosesnya tertahan. Kalau proses itu juga yang menerima data dari lapangan, sore yang buruk bagi seorang admin berubah menjadi sehari penuh submission yang hilang.

Memisahkannya berarti panel boleh tumbang dan penerimaan data tetap jalan. Para enumerator tidak pernah tahu itu terjadi.

Tumpukan teknologinya, dan alasannya

LapisanPilihanAlasan
KlienFlutter (Android dan iOS)Satu basis kode, dan dukungan database lokalnya matang
APIGoSatu binary, remeh untuk di-deploy, cukup cepat sehingga server tak pernah jadi hambatan
DatabasePostgreSQLButuh constraint sungguhan untuk menegakkan idempotensi di lapisan penyimpanan, bukan hanya di kode
ObjekMinIOPenyimpanan foto self-hosted, API S3 tanpa tagihan S3
RuntimeDockerKeseluruhannya empat container dan satu berkas compose
AdminStewardFramework Go buatan saya sendiri, dan Titen adalah tempatnya berjalan di produksi

Self-hosted seluruhnya. Data kliennya tetap berada di infrastruktur yang mereka kendalikan, dan untuk pekerjaan survei yang mencatat nama-nama usaha, itu bukan hal yang bisa ditawar.

Empat minggu, sendirian

Desain produk, dua aplikasi, API-nya, database-nya, infrastruktur dan deployment-nya. Jadwal itulah alasan sebagian keputusan di atas terlihat irit.

Tidak ada kolaborasi realtime, tidak ada antarmuka penyelesaian konflik, tidak ada penggabungan data parsial. Dua enumerator menyunting data yang sama bukan kasus yang ditangani Titen, karena pada penerapan ini hal itu memang tidak bisa terjadi: setiap data melekat pada orang yang membuatnya, dan batasan itu menghemat satu minggu penuh.

Saya akan mengambil keputusan yang sama lagi. Sebagian besar kerumitan offline-first dibayarkan untuk menangani konflik yang sebenarnya tidak diproduksi oleh domainnya.

Status sejujurnya

Titen berjalan dan dipakai di titen-app.my.id. Angka keandalan di atas berasal dari 200 submission uji pada koneksi yang di-throttle dan diputus-putus, bukan dari satu tahun telemetri lapangan. Itu hasil pengukuran, bukan rekam jejak.

Yang belum ada: penyuntingan satu data dari banyak perangkat, peta offline, dan backoff pengiriman ulangnya masih kurva tetap, belum menyesuaikan diri dengan koneksi yang sedang ia amati.

Yang saya bawa dari proyek ini

Kunci idempotensi adalah keputusan terkecil di proyek ini dan ia yang menanggung beban paling berat. Semua yang lain — outbox-nya, backoff-nya, jalur foto yang terpisah — ada supaya sistemnya boleh gagal dengan aman, dan semuanya baru bekerja karena permintaan yang terulang tidak berbahaya.

Itu juga pola yang terus saya pakai sejak itu. Integrasi apa pun yang bisa diulang, dan setiap integrasi bisa diulang, membutuhkan identitas untuk operasinya, bukan identitas untuk datanya. Harganya satu kolom, dan ia menghapus satu kelas bug yang kalau tidak begitu baru akan kamu temukan di produksi, berbulan-bulan kemudian, di sebuah spreadsheet yang jumlahnya tidak cocok.

call to action

Siap membangun proyek bersama saya?

Saya siap membantu membangun, mengembangkan, dan meluncurkan proyek Anda berikutnya — kirim pesan sekarang dan ayo kita mulai!

Mulai Sekarang