Migrasi dari monolith ke microservices yang aman dilakukan bertahap: satu bagian dipindah ke service baru, traffic-nya dialihkan sedikit demi sedikit, sementara monolith tetap melayani sisanya. Polanya disebut Strangler Fig, dari pohon ara yang tumbuh membelit pohon inangnya pelan-pelan sampai pohon lama bisa mati tanpa ada yang roboh. Bayangin renovasi rumah sambil tetap ditinggali. Kamu gak membongkar semua kamar sekaligus, nanti tidur di mana? Satu kamar direnovasi, keluarga pindah sementara ke kamar lain, kamar selesai, lalu kamar berikutnya. Rumahnya tetap bisa dipakai setiap hari.
🧪 Tebakan cepat: selama data e-tiket dipindah ke database baru, kode menulis tiap e-tiket ke tabel lama, lalu ke database baru. Dari 1.000 e-tiket, prosesnya mati 5 kali tepat di antara dua tulisan itu. Berapa e-tiket yang ada di database baru: 1.000 atau 995? Tebak dulu, jawabannya ada di bagian "Dual-write: dua tulisan, satu crash".
Kenapa gak tulis ulang sekaligus
Big bang rewrite artinya menulis ulang seluruh aplikasi dari nol, lalu mengganti yang lama dalam satu peluncuran. Ini hampir selalu molor. Bisnis gak berhenti selama rewrite: promotor tetap minta fitur baru, jadi monolith tetap berubah dan versi baru mengejar target yang bergerak. Perilaku kecil yang gak pernah didokumentasikan hilang di versi baru. Dan risikonya semua atau gak sama sekali: kalau peluncurannya gagal, kembali ke nol. Strangler Fig memberi hasil di tiap langkah, dan tiap langkah bisa dibatalkan sendiri.
Strangler Fig: facade di depan monolith
Kuncinya facade: satu pintu di depan monolith yang menentukan tiap request dilayani siapa. Di TiketNada, facade-nya adalah API gateway api.tiketnada.test dari lesson API Gateway. Ini timeline memindah context tiket (e-tiket dan scan di gerbang) dari monolith app:
| Fase | Yang terjadi | Route di service baru |
|---|---|---|
| 0 | Monolith app, satu PostgreSQL |
0 dari 6 |
| 1 | Facade dipasang di depan app; semua route masih ke app |
0 dari 6 |
| 2 | Tabel e_tiket pindah ke schema tiket di database yang sama |
0 dari 6 |
| 3 | Service tiket jalan; facade kirim 10% GET /api/tickets/:kode ke sana |
1 dari 6 (10%) |
| 4 | Canary sehat: kedua route tiket 100% ke service tiket |
2 dari 6 |
| 5 | Service tiket pindah ke database sendiri, data disinkronkan |
2 dari 6 |
| 6 | Schema lama di-drop, kode tiket dihapus dari monolith | 2 dari 6 |
Langkah pertama yang nyata adalah fase 3: satu endpoint pindah ke service baru, sisanya tetap dilayani monolith. Kalau ada masalah, rollback cukup mengembalikan angka di facade ke 0%, tanpa deploy monolith.
Pilih context pertama
Batas yang dipindah adalah bounded context dari lesson DDD for Microservices, bukan potongan acak. tiket dipilih duluan karena batasnya jelas (ETiket punya model sendiri), ketergantungannya sedikit (cuma butuh kabar "pesanan sudah dibayar" dari order), dan pola bebannya beda: 10.000 scan di gerbang dalam dua jam di hari konser. Mulai dari context yang paling independen supaya risikonya rendah; bayar yang paling sensitif jangan jadi yang pertama.
Memisah database: tiga langkah
Memisah database adalah bagian paling sulit. Urutannya:
- Logical (fase 2): tabel
e_tiketpindah ke schematiket, masih di databaseappyang sama. Query lain yang JOIN ke tabel itu dihapus; modulorderdanbayarmemanggil function modultiket. - API (fase 3): service
tiketberdiri dan jadi satu-satunya yang menyentuh schema itu. Monolith bertanya lewat HTTP, bukanSELECTke tabelnya. - Physical (fase 5): service
tiketpindah ke database sendiri. Selama transisi, data disinkronkan lewat outbox atau CDC.
Membagi database fisik terlalu cepat, sebelum kode berhenti saling query, membuat data gak konsisten selama migrasi.
Dual-write: dua tulisan, satu crash
Di fase 5, schema lama masih jadi sumber kebenaran (supaya bisa rollback), dan database baru sedang diisi. Tiap e-tiket baru harus sampai di dua tempat. Cara paling gampang, dual-write: tulis ke schema lama, lalu tulis ke database baru. Model contoh di Lab: 1.000 e-tiket, dan prosesnya mati di tiap tulisan ke-200:
| E-tiket ke- | Schema lama | Database baru |
|---|---|---|
| 1 sampai 199 | ada | ada |
| 200 | ada (sudah commit) | gak ada: proses mati sebelum tulisan kedua |
| 201 sampai 399 | ada | ada |
| ... 400, 600, 800, 1.000 | ada | gak ada |
| Total | 1.000 | 995 |
Jawaban tebakannya: 995. Lima e-tiket hilang di database baru, dan gak ada error yang memberi tahu. Begitu service tiket membaca database baru, lima pembeli ditolak di gerbang padahal sudah bayar. Membalik urutannya juga gak menolong: kalau database baru ditulis duluan, crash membuat e-tiket ada di yang baru tapi gak ada di sumber kebenaran.
Outbox: satu transaksi, relay yang mengulang
Obatnya outbox pattern dari lesson Data Consistency Patterns: baris e_tiket dan baris outbox (pesan "salin e-tiket ini") ditulis di satu transaksi lokal, jadi dua-duanya ada atau dua-duanya gak ada. Proses terpisah, relay, membaca outbox dan mengirimnya ke database baru.
// Fase 5: 1.000 e-tiket baru selama data pindah ke DB tiket, model contoh
const lama = new Set(); // schema tiket di DB app: sumber kebenaran
const baru = new Set(); // DB tiket yang baru
const outbox = [];
// Tiap e-tiket: baris e_tiket dan baris outbox masuk di SATU transaksi lokal
for (let i = 1; i <= 1000; i++) {
const kode = "TN-42-" + String(i).padStart(4, "0");
lama.add(kode); // INSERT INTO tiket.e_tiket ...
outbox.push({ kode, terkirim: false }); // INSERT INTO tiket.outbox ... lalu COMMIT
}
// Relay: kirim yang belum ditandai, lalu tandai. Mati di antaranya = dikirim ulang.
let dikirim = 0;
let putaran = 0;
while (outbox.some((o) => !o.terkirim)) {
putaran++;
outbox.forEach((o, i) => {
if (o.terkirim) return;
dikirim++;
baru.add(o.kode); // upsert per kode: kiriman ulang gak menggandakan
const relayMati = putaran === 1 && (i + 1) % 200 === 0;
if (!relayMati) o.terkirim = true;
});
}
console.log("lama", lama.size, "| dikirim", dikirim, "| baru", baru.size);
// lama 1000 | dikirim 1005 | baru 1000
- Loop pertama adalah transaksi lokalnya: e-tiket dan pesan outbox-nya masuk bersama, gak ada celah untuk crash di antaranya.
- Relay bisa mati setelah mengirim tapi sebelum menandai
terkirim. Akibatnya bukan kehilangan, tapi kiriman ulang: 5 dari 1.005 kiriman adalah duplikat. baru.add(o.kode)mewakili upsert per kode: menulis e-tiket yang sudah ada gak menambah baris. Penerima wajib idempoten.- Daripada polling tabel outbox, relay juga bisa membaca log database langsung (CDC, Change Data Capture).
Canary: pindahkan traffic sedikit demi sedikit
Di fase 3, facade cuma mengirim 10% GET /api/tickets/:kode ke service baru, dipilih per user (user_id % 100 < 10) supaya satu user selalu mendarat di tempat yang sama. Lalu bandingkan dua jalur dengan sinyal dari lesson Observability in Distributed Systems: error rate 0,2% di keduanya, p95 180 ms di monolith dan 95 ms di service baru (model contoh). Sehat, jadi naik ke 100%. Kalau error rate service baru naik, kembalikan ke 0% dalam hitungan detik.
Kapan berhenti
Migrasi butuh exit criteria: kapan dianggap selesai. Tujuannya bukan monolith kosong, tapi masalah yang selesai. Setelah tiket, lanjut ke konser hanya kalau ada alasan nyata, misalnya halaman event harus kuat 50 kali beban biasa saat war tiket. akun yang jarang berubah dan bebannya kecil boleh tetap di monolith. Tanpa exit criteria, tim terjebak "setengah pindah" selamanya dan merawat dua sistem.
Waspadai hasil yang disebut distributed monolith: service-nya sudah dipisah, tapi masih membaca database yang sama dan saling memanggil secara sinkron di mana-mana. Ongkos jaringannya kamu bayar, kebebasan deploy-nya gak kamu dapat. Kebalikannya juga jebakan: nano-services, service yang terlalu kecil sampai ongkos operasinya lebih besar dari manfaatnya.
⚠️ Jebakan yang sering ketemu
- Big bang rewrite. Molor, mengejar monolith yang terus berubah, dan risikonya semua atau gak sama sekali.
- Dual-write ke dua tempat. Crash di antaranya membuat data berbeda tanpa ada yang tahu. Pakai outbox atau CDC.
- Database dibagi fisik terlalu cepat. Pisahkan schema dulu, ganti query jadi API call, baru pisahkan database.
- Tanpa canary dan facade. Tiap masalah butuh deploy untuk rollback. Dengan facade, rollback cukup ubah angka.
- Tanpa exit criteria. Setengah pindah selamanya berarti dua kali ongkos perawatan.
TL;DR: Pindah dari monolith ke microservices secara bertahap dengan Strangler Fig: pasang facade (API gateway) di depan monolith, lalu pindahkan satu route ke service baru sementara monolith melayani sisanya; big bang rewrite hampir selalu gagal. Pindahkan satu bounded context utuh, mulai dari yang paling independen, seperti tiket. Database dipisah tiga langkah: schema terpisah di database yang sama, ganti query langsung dengan API call, baru database fisik sendiri. Selama data disinkronkan, dual-write kehilangan data saat crash di antara dua tulisan (1.000 jadi 995); outbox menulis baris dan pesannya di satu transaksi lokal, relay mengirim ulang, dan upsert idempoten menjaga hasilnya 1.000. Naikkan traffic lewat canary (10% lalu 100%) sambil membandingkan error rate dan p95. Tentukan exit criteria, dan hindari distributed monolith: service terpisah yang masih berbagi database dan saling memanggil secara sinkron.