Jawaban singkatnya: untuk banyak aplikasi backend Rust, modular monolith lebih tepat sampai tim, domain, dan beban operasional benar-benar menuntut pemisahan layanan. Microservices masuk akal ketika batas domain sudah stabil, ownership tim jelas, kebutuhan scaling berbeda antar domain nyata, dan organisasi siap membayar biaya tambahan pada deployment, observability, serta koordinasi antar layanan.
Jika pertanyaannya adalah "haruskah langsung microservices?", jawabannya biasanya tidak. Dalam praktik, banyak tim kecil sampai menengah lebih cepat bergerak dengan modular monolith yang disiplin: satu deployable unit, tetapi kode dipisah jelas per domain, kontrak antarmuka dijaga, dan dependensi silang dibatasi. Di Rust, pendekatan ini sangat cocok karena workspace dan crate boundaries membantu menjaga modularitas tanpa langsung memaksa kompleksitas distribusi sistem.
Kenapa pertanyaan ini penting khusus untuk backend Rust?
Rust memberi performa tinggi, kontrol resource yang baik, dan type system yang kuat. Namun keunggulan itu tidak otomatis membuat microservices menjadi pilihan terbaik. Arsitektur terdistribusi tetap membawa biaya yang tidak diselesaikan hanya dengan bahasa pemrograman: retry, timeout, tracing lintas service, schema evolution, deployment orchestration, dan debugging jaringan.
Karena itu, keputusan modular monolith vs microservices untuk Rust sebaiknya tidak didorong oleh tren, melainkan oleh kebutuhan operasional dan struktur tim.
Ringkasan keputusan cepat
| Kondisi | Lebih cocok Modular Monolith | Lebih cocok Microservices |
|---|---|---|
| Ukuran tim | 1-8 engineer, ownership masih cair | Beberapa tim mandiri dengan batas domain jelas |
| Kompleksitas deploy | Ingin satu pipeline sederhana | Butuh deploy independen per domain |
| Skala trafik | Pola beban relatif seragam | Subset domain perlu scaling jauh lebih tinggi |
| Biaya infra | Ingin biaya rendah dan tooling sederhana | Siap membayar service mesh, tracing, gateway, queue, dan observability lebih besar |
| Coupling domain | Masih banyak transaksi lintas fitur | Bounded context sudah matang dan relatif otonom |
| Kecepatan delivery | Fokus iterasi produk cepat | Fokus isolasi perubahan dan otonomi tim |
| Kegagalan sistem | Masih bisa ditangani di satu proses | Perlu fault isolation yang nyata antar layanan |
Kapan modular monolith lebih tepat?
Modular monolith adalah satu aplikasi yang dideploy sebagai satu unit, tetapi dipecah menjadi modul atau crate berdasarkan domain yang tegas. Ini bukan "monolith berantakan". Kuncinya adalah batas modul yang disiplin.
1. Tim masih kecil atau bertumbuh, tetapi belum terbagi jelas per domain
Jika 3-6 engineer masih menyentuh banyak area yang sama, memecah menjadi microservices sering menambah overhead koordinasi tanpa memberi manfaat nyata. Anda akan menambah CI/CD pipeline, service discovery, kontrak API, secret management, dan tracing lintas jaringan sebelum masalah itu benar-benar ada.
2. Domain belum stabil
Pada fase awal produk, batas antara billing, user, catalog, atau workflow sering berubah. Jika layanan dipisah terlalu dini, perubahan model domain akan memicu perubahan API antar service, migrasi data yang rumit, dan coupling jaringan yang sulit dibersihkan.
3. Banyak use case butuh transaksi lintas domain
Contoh: pembuatan order membutuhkan validasi user, reservasi inventory, perhitungan promo, dan pencatatan pembayaran. Dalam modular monolith, transaksi database dan orkestrasi sinkron lebih sederhana. Jika domain-domain ini dipisah terlalu cepat, Anda akan masuk ke pola eventual consistency, saga, outbox, dan kompensasi error yang lebih kompleks.
4. Observability dan operasi belum matang
Satu proses lebih mudah di-debug daripada lima service yang saling memanggil. Pada modular monolith, log, metrics, dan tracing masih penting, tetapi blast radius operasional lebih kecil. Anda belum perlu memikirkan distributed tracing secara agresif hanya untuk memahami satu request end-to-end.
5. Biaya infrastruktur perlu dijaga
Microservices hampir selalu meningkatkan biaya: lebih banyak container, pipeline, database atau schema terpisah, ingress, retry policies, dashboard observability, dan alarm. Jika beban bisnis belum membenarkan biaya itu, modular monolith biasanya memberi rasio value/complexity yang lebih baik.
Kapan microservices mulai masuk akal?
Microservices bukan salah; yang salah adalah mengadopsinya terlalu dini. Di backend Rust, microservices mulai masuk akal ketika masalah utama Anda bukan lagi struktur kode internal, tetapi koordinasi tim dan kebutuhan operasional yang berbeda antar domain.
1. Ownership tim sudah jelas dan relatif stabil
Misalnya ada tim Payments, tim Identity, dan tim Notifications yang punya backlog, lifecycle, dan SLA berbeda. Jika setiap tim sering terhambat karena harus menunggu release monolith bersama, pemisahan service bisa mengurangi bottleneck organisasi.
2. Kebutuhan scaling antar domain berbeda jauh
Contoh realistis: service notifikasi menerima lonjakan tinggi karena webhook dan email retries, tetapi modul billing tidak. Pada monolith, Anda menskalakan seluruh aplikasi. Pada microservices, Anda bisa menskalakan notifikasi secara terpisah.
3. Profil performa dan dependensi infrastruktur berbeda
Jika satu domain sangat I/O heavy dan domain lain CPU heavy, pemisahan service memungkinkan tuning runtime, resource, dan deployment policy secara lebih spesifik. Meski demikian, jangan lupa bahwa pemanggilan jaringan lebih mahal daripada function call dalam satu proses.
4. Batas domain sudah matang
Jika kontrak bisnis antara domain relatif stabil, pemisahan service akan lebih tahan lama. Ini penting karena memindahkan boundary yang salah pada arsitektur terdistribusi jauh lebih mahal daripada merapikan boundary internal di monolith.
5. Organisasi siap mengelola failure mode terdistribusi
Begitu masuk microservices, Anda harus nyaman dengan timeout, retry storm, idempotency, queue lag, schema incompatibility, clock skew, dan partial failure. Jika tim belum punya disiplin ini, microservices akan memperbesar masalah yang sebelumnya kecil.
Perbandingan praktis: modular monolith vs microservices
Kompleksitas operasional
- Modular monolith: satu artefak deploy, satu service utama, lebih sedikit moving parts.
- Microservices: banyak artefak, banyak deployment, lebih banyak konfigurasi jaringan dan secret.
Implikasi: jika tim DevOps/SRE kecil atau belum ada, modular monolith biasanya lebih aman.
Biaya infrastruktur
- Modular monolith: lebih hemat compute, networking, dan tooling observability.
- Microservices: biaya naik karena setiap service butuh runtime, health check, autoscaling, logging, metrics, tracing, dan kadang database sendiri.
Observability
- Modular monolith: tracing internal lebih sederhana, korelasi log lebih mudah.
- Microservices: wajib disiplin pada correlation ID, tracing context propagation, dan alerting per service.
Kesalahan umum: tim memecah service lebih dulu, baru memikirkan tracing setelah incident pertama. Urutannya sebaiknya dibalik.
Deployment
- Modular monolith: mudah, tetapi semua perubahan ikut satu release train.
- Microservices: deploy independen memungkinkan, tetapi butuh versioning API dan compatibility management.
Ownership tim
- Modular monolith: cocok ketika ownership masih berbagi dan keputusan lintas domain sering terjadi.
- Microservices: cocok saat tiap domain punya tim yang benar-benar mandiri.
Performa
- Modular monolith: call antar modul berupa function call, latensi rendah, transaksi lokal lebih mudah.
- Microservices: ada overhead serialisasi, jaringan, retry, dan backpressure antar service.
Untuk Rust, keunggulan performa internal akan paling terasa pada modular monolith karena komunikasi in-process sangat murah dibanding RPC.
Maintainability jangka panjang
- Modular monolith: sangat maintainable jika boundary modul dipatuhi. Menjadi buruk jika semua modul saling impor bebas.
- Microservices: maintainable jika kontrak API stabil dan domain benar-benar terpisah. Menjadi buruk jika terlalu banyak chatty calls dan shared database diam-diam.
Desain modular monolith yang sehat di Rust
Kalau memilih modular monolith, targetnya bukan sekadar "satu binary". Targetnya adalah domain terisolasi di level kode. Rust sangat membantu lewat workspace, crate terpisah, dan visibilitas modul yang ketat.
Contoh struktur workspace Rust
backend/
├── Cargo.toml
├── crates/
│ ├── app/ # binary / wiring HTTP, config, startup
│ ├── shared/ # tipe umum, error dasar, util terbatas
│ ├── users/ # domain users
│ ├── billing/ # domain billing
│ ├── orders/ # domain orders
│ ├── notifications/ # domain notifications
│ └── persistence/ # implementasi DB, migrations, adapter
Pola yang sehat:
- app hanya melakukan composition root: routing, dependency injection, startup.
- Setiap domain crate mengekspor API internal yang kecil, bukan detail implementasi.
- shared jangan menjadi tempat semua hal. Jika shared membesar tanpa kontrol, boundary domain mulai bocor.
- persistence sebaiknya menjadi adapter, bukan pusat domain logic.
Batas modul yang disarankan
Dalam setiap crate domain, pisahkan minimal:
- domain: entity, value object, aturan bisnis.
- application: use case, orchestration.
- ports: trait untuk repository, event publisher, payment gateway, dsb.
- adapters: implementasi konkret untuk DB, HTTP client, queue.
// crates/orders/src/lib.rs
pub mod api;
mod application;
mod domain;
mod ports;
mod adapters;
pub use api::{CreateOrder, OrderService};Dengan pola ini, crate lain tidak bisa mengakses detail internal orders sembarangan.
Kontrak antarmuka antar modul
Hindari modul A membaca tabel modul B langsung jika tujuannya masih bisa dicapai lewat antarmuka. Dalam monolith, godaannya besar karena semuanya ada di satu repo dan satu database. Tetapi jika Anda membiarkan akses bebas, migrasi ke service terpisah nanti akan jauh lebih sulit.
// crates/orders/src/api.rs
use crate::domain::{Order, OrderId};
pub struct CreateOrder {
pub user_id: String,
pub items: Vec<String>,
}
pub trait OrderService {
fn create_order(&self, cmd: CreateOrder) -> Result<OrderId, OrderError>;
fn get_order(&self, id: OrderId) -> Result<Order, OrderError>;
}
#[derive(Debug)]
pub enum OrderError {
Validation(String),
UserNotFound,
PaymentRequired,
Internal,
}Prinsip pentingnya:
- Ekspor use case, bukan repository internal.
- Gunakan tipe domain, bukan payload database mentah.
- Jangan expose schema internal sebagai kontrak publik antar modul.
Strategi testing yang cocok
1. Unit test di level domain
Aturan bisnis seperti validasi order, state transition, atau perhitungan biaya harus dites tanpa database dan tanpa HTTP. Ini menjaga kecepatan feedback dan memaksa logika tetap murni.
2. Contract test antar modul
Walau masih satu proses, perlakukan antarmuka crate lain sebagai kontrak. Jika modul users menyediakan API internal untuk validasi user aktif, tulis test yang memastikan perilaku itu stabil. Ini berguna saat nanti modul dipisah menjadi service.
3. Integration test di level persistence
Test repository dengan database nyata atau environment yang mendekati produksi. Fokus pada mapping, constraint, transaction behavior, dan query correctness.
4. End-to-end test secukupnya
Gunakan E2E untuk flow penting saja: registrasi, checkout, pembayaran, notifikasi. Jangan jadikan semua validasi ada di E2E karena biaya perawatan tinggi.
Checklist testing praktis
- Apakah domain logic bisa dites tanpa network dan DB?
- Apakah antarmuka antar modul punya test perilaku?
- Apakah error domain dibedakan dari error infrastruktur?
- Apakah transaction boundary eksplisit dalam use case penting?
Sinyal anti-pattern pada modular monolith
- Shared crate menjadi dumping ground. Jika semua tipe dan helper masuk ke sana, coupling antar domain meningkat.
- Query lintas tabel domain tanpa batas. Cepat di awal, mahal saat migrasi.
- Semua modul tahu detail persistence yang sama. Domain menjadi tergantung schema, bukan kontrak.
- App crate berisi business logic. Composition root seharusnya tipis.
- Build dan test makin lambat karena boundary tidak jelas. Biasanya tanda crate terlalu saling terkait.
Jika anti-pattern ini muncul, masalahnya sering bukan karena monolith, melainkan karena monolith tanpa modularitas.
Sinyal anti-pattern pada microservices
- Service terlalu kecil dan terlalu banyak. Tim menghabiskan waktu mengelola jaringan, bukan fitur.
- Chatty communication. Satu request user memicu banyak panggilan sinkron berantai.
- Shared database antar service. Secara operasional terlihat terpisah, tetapi coupling datanya tetap tinggi.
- Versioning API diabaikan. Deploy service A merusak service B.
- Observability belum siap. Incident sulit dilacak karena trace lintas service tidak lengkap.
- Pemisahan berdasarkan layer teknis, bukan domain. Misalnya service khusus CRUD users, CRUD orders, CRUD payments, tetapi use case tetap saling terikat erat.
Skenario nyata: tim kecil vs tim bertumbuh
Skenario 1: tim kecil, produk masih berubah cepat
Misalnya startup dengan 4 engineer membangun backend Rust untuk marketplace B2B. Fitur order, invoice, user role, approval, dan notifikasi masih sering berubah. Dalam kondisi ini, modular monolith hampir pasti pilihan yang lebih tepat.
Alasannya:
- Batas domain belum final.
- Orang yang sama sering mengubah beberapa area sekaligus.
- Biaya deploy sederhana lebih penting daripada independensi release per domain.
- Transaksi lintas fitur masih dominan.
Fokus teknisnya bukan memecah service, melainkan:
- memisahkan crate per domain,
- mengurangi akses lintas domain langsung,
- menjaga test domain tetap cepat,
- menambahkan tracing dan metrics sejak awal.
Skenario 2: tim bertumbuh, domain notifikasi mulai sangat berbeda
Produk yang sama kemudian memiliki 15 engineer. Modul notifikasi kini menangani email, webhook, retry policy, queue consumer, dan burst traffic tinggi. Domain lain seperti billing dan order lebih stabil. Dalam situasi ini, notifications sering menjadi kandidat pertama untuk dipisah.
Kenapa notifikasi cocok dipisah lebih dulu?
- Dependency eksternal banyak dan berbeda.
- Pola scaling berbeda dari domain inti.
- Failure mode bisa diisolasi.
- Kontraknya biasanya cukup jelas: publish event, kirim notifikasi, catat status.
Ini contoh migrasi yang lebih sehat daripada memecah semua domain sekaligus.
Migrasi bertahap dari modular monolith ke service terpisah
Pendekatan paling aman biasanya mulai dari modular monolith yang rapi, lalu ekstrak domain tertentu saat sinyalnya jelas. Jangan menunggu monolith menjadi kusut total, tetapi juga jangan memisahkan terlalu dini.
Langkah 1: rapikan boundary internal lebih dulu
Sebelum ada service baru, pastikan domain target sudah punya:
- crate terpisah,
- API internal yang jelas,
- akses data tidak diambil langsung oleh modul lain,
- test kontrak yang memadai.
Kalau langkah ini gagal, ekstraksi ke microservice hampir pasti sakit.
Langkah 2: ubah komunikasi menjadi berbasis port
Misalnya orders bergantung pada notifications. Jangan biarkan orders tahu implementasi queue atau SMTP. Definisikan trait atau port internal.
pub trait NotificationPort {
fn order_created(&self, order_id: &str, user_id: &str) -> Result<(), NotifyError>;
}Awalnya implementasi port ini masih in-process. Nanti bisa diganti menjadi HTTP client atau publisher ke broker tanpa mengubah domain orders.
Langkah 3: kenalkan event atau asynchronous boundary
Domain yang akan diekstrak biasanya lebih mudah dipisahkan jika interaksinya tidak bergantung pada request-response sinkron. Contoh: setelah order dibuat, sistem menerbitkan event OrderCreated, lalu notifications memprosesnya. Ini mengurangi coupling temporal.
Jika memakai event, pertimbangkan pola outbox agar update database dan publish event tidak mudah tidak sinkron.
Langkah 4: duplikasi kontrak di batas service, bukan database
Saat modul dipisah, jangan jadikan query langsung ke database lama sebagai jembatan permanen. Itu hanya memindahkan coupling. Lebih baik tetapkan API atau event contract yang jelas, lalu migrasikan konsumen sedikit demi sedikit.
Langkah 5: pindahkan satu domain yang paling siap
Biasanya kandidat awal yang baik:
- notifications,
- search indexing,
- report generation,
- webhook delivery,
- background processing yang independen.
Kandidat awal yang sering buruk:
- order core yang sangat transaksional,
- billing yang masih sering berubah bersama domain lain,
- user/profile jika masih menjadi dependency sinkron hampir semua flow.
Langkah 6: ukur dampaknya
Setelah ekstraksi, evaluasi:
- apakah deployment benar-benar lebih cepat,
- apakah incident berkurang atau justru bertambah,
- apakah ownership tim membaik,
- apakah biaya infra naik sebanding dengan manfaatnya.
Kalau jawabannya tidak, berhenti dulu. Tidak semua modul perlu menjadi service.
Checklist keputusan untuk backend Rust
Pilih modular monolith jika sebagian besar jawaban ini adalah “ya”
- Tim masih kecil atau ownership domain belum mapan.
- Perubahan fitur sering menyentuh banyak domain sekaligus.
- Anda ingin satu pipeline deploy sederhana.
- Banyak transaksi lintas domain butuh konsistensi kuat.
- Biaya infra dan operasional harus ditekan.
- Observability terdistribusi belum matang.
- Batas domain masih dieksplorasi.
Pilih microservices jika sebagian besar jawaban ini adalah “ya”
- Beberapa tim sudah punya domain ownership yang jelas.
- Domain tertentu perlu scaling atau SLA yang sangat berbeda.
- Kontrak antar domain relatif stabil.
- Deploy independen memberi manfaat nyata, bukan sekadar preferensi.
- Tim siap mengelola tracing, retry, timeout, dan partial failure.
- Anda siap membayar biaya infra dan kompleksitas tambahan.
Kesalahan keputusan yang paling umum
- Mengira monolith pasti buruk. Yang buruk adalah boundary yang kabur dan disiplin engineering yang lemah.
- Mengira microservices otomatis scalable. Scaling teknis dan scaling organisasi adalah dua hal berbeda.
- Memisah service berdasarkan dugaan masa depan. Pilih berdasarkan bottleneck nyata, bukan asumsi.
- Mengabaikan ownership tim. Arsitektur harus cocok dengan struktur organisasi yang menjalankannya.
- Terlalu lambat memodularisasi monolith. Monolith yang tidak dijaga akan makin sulit diekstrak nanti.
Penutup
Untuk sebagian besar backend Rust, modular monolith adalah default yang masuk akal karena memberi struktur yang kuat tanpa membayar seluruh biaya sistem terdistribusi. Pilih microservices hanya ketika Anda bisa menunjukkan kebutuhan konkret: batas domain stabil, ownership tim jelas, pola scaling berbeda, dan organisasi siap mengelola kompleksitas operasionalnya.
Jika Anda ragu, mulailah dari workspace Rust yang modular, crate per domain, kontrak antarmuka yang sempit, dan testing yang memisahkan domain dari infrastruktur. Dengan fondasi itu, Anda tidak terkunci pada monolith, tetapi juga tidak membayar mahal untuk microservices sebelum waktunya.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!