Monolit modular cukup ketika kompleksitas utama sistem berasal dari ledakan jalur eksekusi, tetapi kebutuhan operasionalnya belum menuntut isolasi deployment, scaling, atau kegagalan per layanan. Jika tim masih bisa memahami perubahan lintas modul, menguji alur penting secara andal, dan merilis tanpa koordinasi besar antarbagian, memecah sistem terlalu cepat biasanya hanya memindahkan kompleksitas, bukan menguranginya.
Analogi yang berguna datang dari ide video “Can you run every line of code in Super Mario Bros.?”: semakin banyak kemungkinan state dan cabang perilaku, semakin sulit memastikan bahwa kita benar-benar paham sistem tersebut. Di backend, masalah yang sama muncul pada alur seperti checkout, notifikasi, dan pelaporan. Pertanyaannya bukan hanya “apakah sistem besar?”, tetapi “berapa banyak jalur eksekusi yang harus dipahami, diuji, diobservasi, dan dikoordinasikan?” Dari sudut itu, keputusan antara monolit modular, service terpisah, atau event-driven menjadi lebih jelas.
Mengapa sistem menjadi sulit dipahami: bukan ukuran file, tetapi jumlah jalur eksekusi
Banyak tim mengira sistem sulit dipahami karena basis kodenya besar. Sering kali akar masalahnya justru kombinasi berikut:
- Banyak kondisi bisnis: diskon, retry pembayaran, stok habis, timeout, fraud check, fallback notifikasi.
- Efek samping berantai: satu aksi pengguna memicu perubahan data, pengiriman email, update cache, publish event, sinkronisasi ke vendor lain.
- Konsistensi data yang berbeda-beda: sebagian alur harus sinkron dan atomik, sebagian cukup eventual consistency.
- State tersembunyi: job async, cron, queue retry, dead-letter, cache invalidation, dan konfigurasi environment.
Dalam kondisi seperti ini, memecah aplikasi menjadi banyak service tidak otomatis menyederhanakan sistem. Jalur eksekusi yang tadinya intra-process berubah menjadi network hop, message broker, retry policy, versioning event, dan kebutuhan tracing. Kompleksitas berpindah dari struktur kode ke koordinasi runtime.
Contoh konkret: checkout
Checkout tampak seperti satu fitur, tetapi biasanya memiliki cabang seperti:
- stok cukup atau tidak,
- pembayaran sukses, pending, gagal, timeout, atau callback ganda,
- voucher valid atau invalid,
- ongkir berubah,
- notifikasi pelanggan dan gudang,
- pelaporan ke data warehouse.
Jika semua ini dipecah terlalu dini menjadi banyak service, Anda menambah kemungkinan kegagalan jaringan, duplikasi event, race condition, dan kebutuhan observability lintas proses. Sistem menjadi lebih terdistribusi, tetapi belum tentu lebih mudah dipahami.
Kapan monolit modular cukup
Monolit modular adalah satu aplikasi deployable dengan batas modul yang jelas di dalam kode: misalnya modul Checkout, Inventory, Notification, Billing, dan Reporting. Ia cukup ketika Anda butuh pemisahan tanggung jawab yang tegas, tetapi belum butuh beban operasional arsitektur terdistribusi.
Sinyal bahwa monolit modular masih tepat
- Transaksi bisnis utama masih saling terikat erat. Misalnya checkout, reservasi stok, dan pencatatan order harus konsisten dalam satu boundary.
- Tim belum terlalu besar. Jika sebagian besar perubahan masih bisa dikoordinasikan dalam satu repo dan satu pipeline, monolit modular biasanya lebih efisien.
- Deployment bersama masih dapat diterima. Anda belum sering mengalami kebutuhan rilis independen yang saling berbenturan.
- Skala beban belum sangat timpang. Misalnya notifikasi memang ramai, tetapi belum sampai memaksa scaling terpisah dengan alasan biaya atau performa.
- Observability terdistribusi belum matang. Jika tracing, correlation ID, idempotency, dan event monitoring belum disiplin, microservices justru memperburuk debugging.
- Banyak bug berasal dari logika domain, bukan dari bottleneck infrastruktur. Artinya yang perlu dibenahi dulu adalah desain modul, kontrak internal, dan testability.
Ciri monolit modular yang sehat
Monolit modular bukan sekadar “satu repo besar”. Ia butuh batas yang nyata:
- Dependency direction jelas: modul tidak saling memanggil sembarangan.
- API internal eksplisit: akses antar-modul lewat interface atau service boundary, bukan query database langsung ke tabel modul lain.
- Domain model terlokalisasi: aturan checkout tidak bocor ke modul notification.
- Pengujian berlapis: unit test untuk domain, integration test untuk kontrak modul, end-to-end untuk alur kritis.
Contoh struktur modul
src/
checkout/
application/
domain/
infrastructure/
api/
inventory/
application/
domain/
infrastructure/
api/
notification/
application/
domain/
infrastructure/
api/
reporting/
application/
domain/
infrastructure/
api/Struktur seperti ini membantu karena batas domain terlihat di level folder, dependency bisa direview lebih mudah, dan refactor tidak langsung berarti pecah service.
Kapan perlu service terpisah
Memecah layanan masuk akal jika manfaat isolasi operasional lebih besar daripada biaya koordinasinya.
Sinyal kuat untuk memecah layanan
- Kebutuhan scaling sangat berbeda. Contoh: Notification harus menangani lonjakan besar dari email, push, atau webhook, sementara Checkout lebih sensitif ke latency dan konsistensi.
- Siklus rilis independen benar-benar dibutuhkan. Misalnya tim notifikasi sering merilis perubahan integrasi vendor tanpa ingin membawa risiko deploy ke alur order.
- Profil kegagalan berbeda dan perlu isolasi. Gangguan pada pelaporan tidak boleh mengganggu transaksi checkout.
- Batas domain sudah stabil. Anda tahu dengan cukup jelas mana data dan aturan yang menjadi milik satu layanan.
- Ketergantungan organisasi mendukung. Ada tim yang bisa benar-benar memiliki service tersebut, termasuk on-call, observability, kapasitas, dan keamanan.
Contoh yang sering layak dipisah
Notification adalah kandidat umum. Alasannya:
- integrasi eksternal banyak dan berubah-ubah,
- traffic bursty,
- bisa dijalankan async,
- kegagalannya sering dapat diisolasi dari transaksi utama.
Reporting juga sering layak dipisah jika query analitik mulai mengganggu workload transaksional. Membiarkan modul reporting menembak query berat ke database utama adalah penyebab umum performa buruk pada monolit.
Sebaliknya, Checkout sering lebih aman tetap dekat dengan inventory dan order management pada tahap awal, karena membutuhkan koordinasi state yang ketat.
Kapan event-driven membantu, dan kapan justru menambah kabut
Arsitektur event-driven berguna ketika beberapa proses memang tidak perlu sinkron, dan Anda ingin mengurangi coupling langsung. Tetapi event bukan solusi universal; ia menukar kesederhanaan request-response dengan kompleksitas asinkron.
Cocok untuk event-driven
- Efek samping pasca-transaksi: setelah order dibuat, kirim notifikasi, update search index, dorong data ke reporting.
- Integrasi antar domain yang longgar: modul lain cukup tahu bahwa “OrderPlaced” terjadi, tanpa perlu memanggil API checkout secara sinkron.
- Pemrosesan yang bisa retry: pengiriman email, sinkronisasi ke sistem eksternal, enrichment data.
Tidak cocok jika
- Alur butuh jawaban langsung dan atomik. Misalnya otorisasi pembayaran dan reservasi stok tidak boleh menggantung tanpa keputusan jelas.
- Tim belum siap mengelola idempotency, ordering, dan observability.
- Anda menggunakan event hanya untuk menghindari mendesain boundary yang jelas.
Contoh event internal pada monolit modular
Anda tidak harus langsung punya broker terpisah untuk menerapkan gaya event-driven. Dalam monolit modular, event internal bisa dipakai untuk mengurangi coupling tanpa membangun distributed system penuh.
// Pseudocode
function placeOrder(command) {
const order = checkoutService.createOrder(command)
domainEvents.publish({
type: 'OrderPlaced',
orderId: order.id,
customerId: order.customerId,
total: order.total
})
return order
}
subscribe('OrderPlaced', async (event) => {
await notificationService.sendOrderConfirmation(event.orderId)
})
subscribe('OrderPlaced', async (event) => {
await reportingService.recordOrder(event)
})Pola ini bekerja karena Checkout tidak perlu tahu detail notifikasi dan pelaporan. Jika nanti notifikasi benar-benar perlu dipisah menjadi service, kontrak event sudah mulai terbentuk. Namun tetap perlu disiplin: event harus punya makna domain yang jelas, bukan sekadar bocoran detail database.
Matriks keputusan: monolit modular vs service terpisah vs event-driven
| Kriteria | Monolit Modular | Service Terpisah | Event-Driven |
|---|---|---|---|
| Pemahaman kode | Tinggi jika boundary internal rapi | Menurun jika boundary lemah dan banyak hop jaringan | Bisa membaik untuk coupling, tapi lebih sulit ditelusuri end-to-end |
| Biaya operasional | Paling rendah | Lebih tinggi: deployment, network, auth, observability | Tinggi jika memakai broker, retry, DLQ, schema governance |
| Deployment independen | Terbatas | Kuat | Kuat untuk consumer terpisah, tetapi kontrak event harus dijaga |
| Isolasi kegagalan | Rendah sampai sedang | Lebih baik jika benar-benar terisolasi | Baik untuk proses async, buruk jika backlog menumpuk tanpa kontrol |
| Konsistensi transaksi | Paling sederhana | Lebih sulit lintas service | Biasanya eventual consistency |
| Observability | Lebih mudah | Perlu tracing dan korelasi request | Perlu tracing event, retry visibility, dan monitoring consumer |
| Koordinasi tim | Mudah untuk tim kecil-menengah | Baik jika ownership tim jelas | Baik jika kontrak event matang; buruk jika event liar |
| Maintainability jangka panjang | Baik jika modularitas dijaga | Baik jika boundary tepat; buruk jika pemecahan prematur | Baik untuk integrasi longgar; buruk jika event jadi saluran semua hal |
Aturan praktis: mulai dari monolit modular, tambahkan event internal untuk efek samping yang tidak kritis, lalu pisahkan service hanya ketika ada tekanan operasional yang nyata dan berulang.
Contoh keputusan pada tiga konteks nyata
1) Checkout
Biasanya tetap di monolit modular lebih lama. Checkout berisi banyak aturan bisnis yang saling bergantung: validasi cart, harga, promo, inventory, payment state, dan order lifecycle. Menjaganya tetap dalam satu process memudahkan transaksi, debugging, dan pengujian alur kritis.
Pemisahan terlalu cepat berisiko menimbulkan:
- kompensasi lintas service yang rumit,
- inconsistency antara stok, pembayaran, dan order,
- latency tambahan pada jalur paling sensitif.
2) Notifikasi
Sering cocok dipisah lebih awal. Ia punya karakteristik berbeda: banyak integrasi eksternal, retry, throughput bursty, dan sifat async. Bahkan jika belum jadi service terpisah, notifikasi hampir selalu layak dipisahkan secara modular dan dijalankan lewat queue.
Debugging yang perlu disiapkan:
- idempotency key agar callback atau retry tidak mengirim pesan ganda,
- status pengiriman per channel,
- dead-letter handling untuk vendor yang sering timeout.
3) Pelaporan
Layak dipisah ketika query analitik mengganggu transaksi. Banyak sistem rusak bukan karena domain checkout buruk, tetapi karena reporting menarik query berat dari database yang sama. Memindahkan pelaporan ke pipeline event atau replica terpisah sering memberi manfaat besar tanpa memecah domain transaksional utama.
Biaya tersembunyi yang sering diabaikan
1) Observability
Dalam monolit, stack trace dan log sering cukup untuk mulai investigasi. Dalam sistem terdistribusi, Anda perlu:
- correlation ID lintas request dan event,
- distributed tracing,
- metric queue depth, retry count, consumer lag,
- dashboard yang membedakan error bisnis dan error infrastruktur.
Tanpa ini, memecah service justru membuat sistem lebih sulit dipahami daripada monolit yang rapi.
2) Deployment dan operasional
Setiap service tambahan berarti pipeline CI/CD, secret management, rate limit, health check, rollback strategy, dan kebijakan kapasitas tersendiri. Secara teori ini memberi fleksibilitas, tetapi secara praktik menambah permukaan masalah.
3) Koordinasi tim
Service boundary yang baik seharusnya mengurangi koordinasi. Jika setiap perubahan fitur masih butuh sinkronisasi 3-4 tim, Anda mungkin belum memecah di boundary yang benar. Hasilnya: overhead bertambah, otonomi tidak tercapai.
Anti-pattern memecah terlalu cepat
- Database per service di atas kertas, join manual di kenyataan. Jika business flow tetap butuh data lintas service secara ketat, Anda hanya memindahkan join dari SQL ke network.
- Event sebagai RPC terselubung. Producer menerbitkan event sambil berharap consumer tertentu segera bereaksi agar transaksi “selesai”. Ini coupling sinkron yang disamarkan.
- Shared library domain raksasa. Semua service bergantung pada paket bersama yang terus berubah, sehingga independensi deployment hilang.
- Memecah berdasarkan layer teknis, bukan domain. Misalnya service user, service database, service email, tetapi alur bisnis tetap tersebar ke mana-mana.
- Memisahkan demi tren, bukan tekanan nyata. Jika tidak ada masalah scaling, isolasi, atau ownership tim, pecah service biasanya terlalu dini.
Panduan implementasi praktis: mulai dari monolit modular yang siap berevolusi
1) Tegakkan boundary di level kode
Gunakan interface internal, package/module boundary, dan review dependency. Atur agar modul checkout tidak bebas mengakses repository modul lain tanpa kontrak.
2) Pisahkan jalur sinkron dan async
Alur inti seperti pembuatan order tetap sinkron dan jelas. Efek samping seperti email, webhooks, dan pelaporan dipindahkan ke queue atau event internal.
3) Lindungi database utama dari reporting
Jika laporan berat, kirim data ringkas ke storage atau replica yang lebih cocok. Ini sering memberi dampak besar tanpa perlu memecah semua service.
4) Siapkan kontrak sebelum memecah
Sebelum notification menjadi service terpisah, definisikan dulu input-output yang stabil: payload, idempotency key, retry semantics, dan error handling. Jika belum jelas di monolit, biasanya belum siap dipisah.
5) Ukur sebelum memutuskan
Kumpulkan sinyal konkret:
- fitur mana yang paling sering gagal dirilis,
- modul mana yang paling sulit diuji,
- jalur mana yang paling sering menyebabkan incident,
- workload mana yang paling tidak seimbang,
- bagian mana yang paling sering memaksa koordinasi lintas tim.
Keputusan arsitektur yang baik muncul dari tekanan sistem yang nyata, bukan dari asumsi umum bahwa “microservices lebih scalable”.
Checklist keputusan singkat
- Apakah masalah utama Anda ada pada kompleksitas domain atau beban operasional?
- Apakah boundary domain sudah cukup jelas untuk dimiliki tim secara independen?
- Apakah kebutuhan scaling, deployment, dan isolasi kegagalan memang berbeda secara signifikan?
- Apakah observability dan operasi async Anda sudah matang?
- Apakah pemisahan akan mengurangi jalur eksekusi yang harus dipahami, atau justru menambah hop baru?
Jika sebagian besar jawaban masih condong ke kompleksitas domain internal, monolit modular biasanya cukup dan sering kali merupakan pilihan yang lebih aman. Jika tekanan operasional, ownership tim, dan perbedaan karakter workload sudah kuat, pemisahan service mulai masuk akal. Dan jika yang Anda butuhkan adalah mengendurkan coupling untuk efek samping non-kritis, event-driven bisa menjadi alat yang tepat, selama Anda siap dengan konsekuensi asinkronnya.
Penutup
Sistem yang sulit dipahami jarang membaik hanya karena dipecah menjadi lebih banyak proses. Seperti analogi jalur eksekusi pada game, semakin banyak cabang, state, dan interaksi, semakin mahal biaya untuk memahami dan memverifikasi perilakunya. Monolit modular cukup selama ia masih menurunkan kompleksitas kognitif, menjaga alur inti tetap jelas, dan menunda biaya distribusi yang belum perlu dibayar. Pecah layanan ketika ada alasan teknis dan operasional yang nyata, bukan sekadar karena sistem terasa “sudah besar”.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!