Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.
01Bagaimana cara migrasi monolith ke microservices dengan strangler fig pattern?
Pasang facade atau reverse proxy di depan monolith, lalu ekstrak satu slice per waktu ke service baru dan arahkan path itu ke sana. Pindahkan data milik slice dengan outbox atau change data capture, naikkan traffic bertahap per persen, dan sediakan flag agar bisa kembali. Ulangi sampai sisa monolith kecil dan stabil.
02Apa beda strangler fig pattern dengan rewrite big-bang?
Rewrite big-bang mengirim seluruh nilai dan risikonya pada satu hari cut-over. Strangler pattern mengirim satu slice yang bisa dibatalkan per waktu sementara sistem lama tetap berjalan. Kesalahan hanya merugikan satu slice, bukan seluruh sistem.
03Bagaimana menangani database saat memecah monolith?
Pastikan tepat satu sistem memiliki setiap data. Hindari dual write karena dua write tidak berada dalam satu transaksi dan bisa tidak sinkron secara diam-diam. Gunakan transactional outbox atau change data capture agar service baru membangun salinannya dari event, lalu pindahkan kepemilikan write setelah kedua salinan cocok.
04Bisakah melakukan rollout bertahap per persen dengan nginx?
Bisa. Directive split_clients melakukan hash pada sebuah string, misalnya alamat client, lalu memetakan rentang hash ke persentase yang Anda tulis. Client yang sama selalu mendarat di backend yang sama, dan rollback cukup mengubah persentase menjadi 0 lalu reload nginx.
05Kapan sebaiknya tidak migrasi ke microservices?
Jangan migrasi jika tim kecil dan masalah sebenarnya adalah release lambat, jika kode tidak punya batas modul yang jelas, jika sistem saat ini tidak bisa diobservasi, atau jika alasannya tren dan bukan masalah terukur. Modular monolith sering menjadi solusi yang lebih murah. Microservices menambah kegagalan jaringan dan data terdistribusi yang jarang sepadan di setup satu server.
Strangler Fig Pattern: Migrasi Monolith ke Microservices
Cara migrasi monolith ke microservices dengan strangler fig pattern: facade routing, satu slice per langkah, kepemilikan data, rollback, dan kapan berhenti.
Strangler fig pattern memigrasikan monolith ke microservices tanpa rewrite. Pasang facade di depan sistem, arahkan satu path per waktu ke service baru, beri service itu data sendiri lewat outbox atau change data capture, sediakan routing flag untuk rollback instan, lalu berhenti ketika sisa monolith sudah stabil.
Kebanyakan monolith ERP dan POS bukan kode yang buruk. Itu kode yang berjalan baik tetapi tidak ada yang berani menyentuhnya, karena layar invoice, ledger stok, dan printer struk berbagi satu codebase dan satu database. Jawaban yang menggoda adalah rewrite. Hasil yang biasanya muncul adalah dua sistem yang harus dirawat lama dan akhir pekan cut-over yang ditakuti semua orang.
Post ini membahas alternatifnya: strangler fig pattern. Isinya facade routing, urutan slice yang diekstrak, bagian tersulit (siapa pemilik data), branch by abstraction, anti-corruption layer, rollback, dan tanda-tanda bahwa Anda sebaiknya tidak bermigrasi sama sekali. Polanya berasal dari Martin Fowler dan Azure Architecture Center, dan directive nginx dicek terhadap dokumentasi nginx. Contohnya adalah worked example ilustratif, bukan hasil pengukuran dari migrasi produksi.
Apa itu strangler fig pattern, dan kenapa tidak rewrite saja?
Martin Fowler menamai pola ini dari pohon strangler fig, tanaman rambat yang tumbuh mengelilingi pohon inang dan perlahan menggantikannya. Dalam software, Anda membangun sistem baru di sekeliling sistem lama dan membiarkannya tumbuh sampai sistem lama bisa dimatikan. Azure Architecture Center menjelaskan ide yang sama sebagai penggantian bertahap fungsi tertentu dengan aplikasi dan service baru, sementara sebuah facade mengarahkan request ke implementasi lama atau baru.
Alasan pola ini mengalahkan rewrite adalah bentuk risikonya. Rewrite mengirim seluruh nilai dan seluruh risikonya dalam satu hari. Migrasi strangler mengirim satu slice kecil per waktu, masing-masing bisa dibatalkan, sehingga kesalahan hanya merugikan satu slice, bukan seluruh sistem. Kalau Anda masih menimbang apakah perlu memecah sama sekali, baca dulu perbandingan di post microservices versus monolith, karena pola ini mengasumsikan tujuan akhirnya memang service terpisah.
Bagaimana facade mengarahkan request per path?
Facade adalah reverse proxy yang dipanggil semua client. Awalnya ia meneruskan semuanya ke monolith, jadi tidak ada yang berubah bagi pengguna. Ketika service pertama siap, Anda menambahkan satu aturan path. Config di bawah memakai nginx, yang cocok untuk satu VPS dengan Docker: aturan path menentukan apa yang boleh pindah, dan blok split_clients menentukan berapa persen traffic itu yang benar-benar menuju service baru.
# /etc/nginx/conf.d/facade.conf (the strangler facade)
upstream legacy_erp { server 127.0.0.1:3000; } # the monolith, untouched
upstream orders_svc { server 127.0.0.1:4001; } # the first extracted slice
# Percentage split. split_clients hashes the string below, so the same client
# lands in the same bucket on every request: no flip-flopping mid-session.
# Rollback = change 10% to 0% and reload. No deploy needed.
split_clients "$remote_addr-orders" $orders_backend {
10% orders_svc;
* legacy_erp;
}
server {
listen 80;
server_name erp.example.com;
# Path rule: only /api/orders/ is eligible to leave the monolith.
location /api/orders/ {
proxy_set_header X-Served-By $orders_backend; # lets you grep logs per backend
proxy_pass http://$orders_backend;
}
# Everything else is still the monolith, exactly as before.
location / {
proxy_pass http://legacy_erp;
}
}
Directive split_clients, menurut dokumentasi nginx, membuat variabel untuk A/B testing: ia melakukan hash terhadap string yang Anda berikan lalu memetakan rentang hash ke persentase yang Anda daftarkan, dengan tanda bintang sebagai sisanya. Karena input hash di sini adalah alamat client ditambah label, satu client selalu mendarat di backend yang sama, sehingga pengguna tidak melihat perilaku berbeda di setiap refresh. Kalau Anda butuh stickiness per user yang login, lakukan hash pada cookie atau header.
Catat nama backend di setiap request (header X-Served-By di atas, atau field log_format). Saat ramp-up Anda akan ingin membandingkan error rate jalur lama dan baru berdampingan, dan tanpa field per backend perbandingan itu hanya tebakan.
Worked example: anggap path orders menerima 20.000 request per hari. Pada 10 persen, service baru melihat sekitar 2.000 request, cukup untuk memunculkan edge case yang buruk dalam sehari sementara 18.000 request masih dilayani monolith. Naik ke 25 persen berarti 5.000, lalu 50 persen berarti 10.000, lalu 100 persen. Angka-angka ini hanya aritmetika dari volume yang diasumsikan, bukan benchmark, tetapi menunjukkan kenapa ramp berbasis persentase menemukan masalah dengan blast radius yang terbatas.
Slice mana yang sebaiknya diekstrak lebih dulu?
Pilih slice pertama untuk belajar, bukan untuk gengsi. Anda butuh sesuatu yang batasnya sudah terlihat, datanya sebagian besar miliknya sendiri, dan kegagalannya bisa ditoleransi. Nilai kandidat dengan kriteria ini sebelum menyentuh kode apa pun.
Kandidat slice
Pilihan pertama yang baik jika
Tanda bahaya
Reporting atau search read-only
Hanya membaca data dan hasil yang terlambat beberapa detik masih bisa diterima
Melakukan join ke sepuluh tabel milik modul yang berbeda
Notifikasi, email, cetak struk
Dipicu oleh event dan punya sedikit dependency masuk
Membaca separuh tabel bisnis untuk menyusun pesannya
Kalkulasi harga atau pajak
Sebagian besar logika murni dengan input dan output yang jelas
Aturannya tersebar di stored procedure dan kode UI
Posting ledger atau inventory inti
Hampir tidak pernah jadi yang pertama; ekstrak paling akhir, kalau perlu
Setiap modul lain menulis ke sini dalam satu transaksi
Reporting dan notifikasi mengajarkan mekanik routing, deployment, dan observability dengan murah. Simpan inti transaksional sampai tim sudah mengulang seluruh loop beberapa kali.
Siapa pemilik data setelah slice diekstrak?
Routing adalah bagian yang mudah. Data adalah bagian yang sulit, karena service yang masih membaca dan menulis database monolith adalah distributed monolith dengan tambahan network hop. Tujuannya adalah tepat satu sistem memiliki setiap data, dan yang lain mendapatkannya lewat API atau event. Ada tiga cara umum untuk sampai ke sana selama masa transisi.
Pendekatan
Cara kerja
Risiko utama
Shared database (sementara)
Service baru membaca dan menulis langsung ke tabel monolith
Perubahan schema merusak dua sistem; coupling tersembunyi
Dual write
Aplikasi menulis ke store lama dan baru dalam satu request
Tidak ada transaksi bersama, jadi satu write bisa berhasil dan yang lain gagal
Outbox atau change data capture
Tulis sekali, publikasikan perubahan dari log atau tabel outbox
Consumer harus idempotent; stream menambah latency
Dual write terlihat sederhana dan gagal secara diam-diam. Aritmetika ilustratif: jika 20.000 write per hari masing-masing punya peluang 0,1 persen write kedua gagal setelah yang pertama berhasil, itu berarti 20 record tidak sinkron setiap hari, dan tidak ada yang memberi tahu record mana. Solusinya adalah menjadikan satu write otoritatif dan menurunkan yang lain darinya. Transactional outbox melakukan itu: baris bisnis dan baris event di-commit dalam satu transaksi lokal, lalu relay mempublikasikan event sesudahnya. Versi Postgres lengkapnya ada di post transactional outbox.
-- Same transaction: the business row and the event row commit together or not at all.
BEGIN;
UPDATE orders SET status = 'PAID' WHERE id = 8841;
INSERT INTO outbox (aggregate_id, event_type, payload)
VALUES (8841, 'OrderPaid', '{"orderId": 8841, "amount": 250000}');
COMMIT;
-- A relay (a poller, or CDC reading the WAL) publishes outbox rows to the new
-- service afterwards. If the relay dies, the rows are still there. A dual write
-- (DB write, then HTTP call) loses the event the moment the process dies between them.
Change data capture mencapai hasil yang sama dengan membaca log database alih-alih tabel outbox, dan tool seperti Debezium melakukannya untuk Postgres. Dengan cara apa pun, service baru membangun salinan datanya sendiri dari event, dan setelah benar Anda mengarahkan write ke service baru lalu menghentikan monolith menulis ke tabel lama.
Jangan biarkan shared database terus ada. Service yang membaca tabel monolith di balik facade lolos semua tes routing namun tetap menghalangi setiap perubahan schema, jadi jadwalkan cut-over kepemilikan data sebagai langkah eksplisit dengan tanggal.
Di mana posisi branch by abstraction dan anti-corruption layer?
Facade berbasis path hanya bekerja jika slice bisa dijangkau lewat HTTP. Ketika kode yang diganti dipanggil in-process dari banyak tempat, gunakan branch by abstraction seperti dijelaskan Martin Fowler: pasang interface di depan implementasi lama, pindahkan caller ke interface itu, bangun implementasi baru di baliknya, lalu pindah dengan flag. Pola anti-corruption layer dari Azure Architecture Center menyelesaikan masalah di sebelahnya. Ia adalah lapisan terjemahan antara dua subsystem dengan model berbeda, sehingga kosakata service baru tidak bocor ke sistem lama atau sebaliknya. Pada sketsa di bawah, adapter melakukan keduanya: interface membawa branch dan adapter menerjemahkan.
// Branch by abstraction: callers depend on the interface, never on a backend.
interface OrderPricing {
quote(cartId: string): Promise<Quote>;
}
class LegacyPricing implements OrderPricing {
async quote(cartId: string) {
return legacyPriceCart(cartId); // in-process call into the monolith
}
}
// The anti-corruption layer lives inside this adapter, so the new service's
// vocabulary never leaks into the old code or the other way round.
class PricingServiceClient implements OrderPricing {
async quote(cartId: string) {
const res = await fetch("http://pricing:4002/quotes", {
method: "POST",
body: JSON.stringify({ cartId }),
});
const dto = await res.json();
return {
total: dto.grandTotalMinor / 100, // service speaks minor units
status: dto.state === "FINAL" ? "C" : "P", // monolith still expects "C" / "P"
};
}
}
// The flag decides at runtime, so rolling back is a config flip, not a revert.
export const pricing: OrderPricing = flags.get("pricing.useService")
? new PricingServiceClient()
: new LegacyPricing();
Simpan terjemahan di satu adapter. Jika monolith mengharapkan status satu huruf dan service baru mengembalikan kata, konversi di satu file itu, dan pada hari monolith dipensiunkan Anda menghapus adapter beserta keanehannya.
Bagaimana cara rollback, dan kapan berhenti?
Rollback harus berupa perubahan konfigurasi, bukan redeploy. Dengan facade di atas, mengubah split menjadi 0 persen lalu reload nginx mengembalikan semua traffic ke monolith dalam hitungan detik, selama monolith masih bisa bekerja dengan datanya. Syarat terakhir itu jebakannya: begitu service baru memiliki write, rollback butuh data mengalir balik, jadi biarkan relay berjalan dua arah sampai slice terbukti.
Naikkan persentase bertahap dan tahan tiap langkah cukup lama untuk melihat satu siklus bisnis normal, misalnya satu hari dagang penuh untuk POS.
Tentukan pemicu rollback sebelum ramp: error rate, batas latency, atau jumlah selisih rekonsiliasi.
Hapus jalur kode lama hanya setelah jalur baru membawa 100 persen traffic melewati setidaknya satu tutup buku periode penuh.
Tahu kapan berhenti sama pentingnya. Tujuannya bukan monolith dengan nol baris tersisa, melainkan sistem di mana inti yang tersisa stabil, jarang berubah, dan murah dijalankan. Jika sisa monolith memenuhi standar itu, membiarkannya adalah akhir yang sah, dan modular monolith sering menjadi rumah yang lebih baik untuk sisanya.
Kapan sebaiknya tidak bermigrasi sama sekali?
Pola ini adalah alat untuk rasa sakit tertentu, dan banyak tim memakainya tanpa rasa sakit itu. Anggap berikut sebagai checklist, dan jika dua atau lebih berlaku, perbaiki monolith dulu.
Tim kecil dan masalah sebenarnya adalah release lambat atau tes yang flaky, yang bisa diatasi modular monolith dan pipeline lebih baik dengan biaya lebih murah.
Tidak ada batas modul yang jelas di kode, sehingga Anda hanya menebak di mana harus memotong.
Anda tidak bisa mengobservasi sistem saat ini: tidak ada log, metric, atau trace per request, sehingga tidak bisa membedakan regresi dari perilaku normal.
Pendorongnya adalah tren atau pitch rekrutmen, bukan masalah terukur seperti scaling independen, isolasi release, atau tim terpisah yang memiliki domain itu.
Microservices menambah failure mode jaringan, data terdistribusi, dan beban operasional. Pada satu VPS dengan satu instance Postgres, biaya itu nyata sedangkan manfaatnya sering tidak.
Seperti apa checklist langkah demi langkahnya?
Jalankan loop ini sekali per slice. Setiap putaran harus cukup kecil untuk selesai dalam beberapa minggu dan bisa dibatalkan di setiap langkah.
Pasang facade di depan monolith dan arahkan 100 persen traffic lewat facade tanpa perubahan. Pastikan tidak ada regresi.
Tambahkan logging per backend dan dashboard sebelum mengekstrak apa pun.
Pilih satu slice memakai tabel di atas dan tentukan API serta kepemilikan datanya.
Buat abstraction atau adapter, lalu bangun service baru di baliknya dengan anti-corruption layer.
Sinkronkan data dengan outbox atau change data capture, lalu rekonsiliasi lama dan baru sampai cocok.
Naikkan split bertahap sambil memantau pemicu rollback sampai slice membawa 100 persen.
Pindahkan kepemilikan data, hapus jalur kode lama, dan putuskan apakah slice berikutnya layak dikerjakan.
Aturan yang layak dibawa: jangan migrasi dengan mengganti, migrasilah dengan routing. Facade membuat setiap langkah bisa dibatalkan, aturan satu pemilik per tabel menjaga data tetap jujur, dan kondisi berhenti yang jelas mencegah proyek menjadi rewrite kedua. Ekstrak slice yang paling banyak mengajari Anda, buktikan, lalu tanyakan lagi apakah yang berikutnya sepadan dengan biayanya.