Saat membahas merancang sistem tahan insiden, pertanyaan utamanya bukan “arsitektur mana yang paling modern”, melainkan “bagaimana membatasi dampak saat sesuatu di luar kendali terjadi”. Tidak semua gangguan datang dari bug internal. Ada juga kejadian eksternal yang tidak bisa diprediksi, seperti berita pesawat Delta yang terkena kembang api saat mendarat: sistem inti tetap berjalan, tetapi ada gangguan dari luar yang bisa berdampak serius bila tidak ada pemisahan risiko.
Analogi itu relevan untuk backend. Payment gateway bisa timeout, provider OTP bisa rate limit, partner logistics bisa mengirim webhook rusak, atau traffic spike bisa menjatuhkan satu jalur kritis. Arsitektur yang baik bukan yang menghilangkan semua insiden, tetapi yang memastikan satu masalah tidak melumpuhkan seluruh sistem. Di titik inilah pilihan antara monolit modular, service terpisah, event-driven, dan komponen terisolasi menjadi keputusan teknik yang perlu dipikirkan secara sadar.
Mengapa blast radius lebih penting daripada sekadar “scalable”
Banyak tim terlalu cepat berbicara tentang skalabilitas horizontal, padahal masalah produksi sehari-hari sering kali lebih dekat ke blast radius: seberapa jauh dampak kegagalan menyebar. Jika modul invoice lambat, apakah checkout ikut gagal? Jika consumer event macet, apakah API login juga ikut terpengaruh? Jika satu deploy bermasalah, apakah seluruh aplikasi harus rollback?
Blast radius ditentukan oleh beberapa hal:
- Shared runtime: apakah semua komponen berjalan dalam proses atau cluster yang sama?
- Shared database: apakah lock, query lambat, atau migrasi satu domain dapat memengaruhi domain lain?
- Coupling saat runtime: apakah request harus memanggil service lain secara sinkron agar berhasil?
- Coupling saat deploy: apakah perubahan kecil memaksa release besar?
- Shared operational surface: apakah logging, queue, cache, dan network dependency dipakai bersama tanpa isolasi?
Intinya, makin banyak resource dibagi bersama tanpa pagar yang jelas, makin besar area terdampak saat insiden terjadi.
Pilihan arsitektur untuk merancang sistem tahan insiden
1. Monolit modular
Monolit modular adalah satu aplikasi yang dideploy sebagai satu unit, tetapi domain dipecah jelas menjadi modul dengan batas tanggung jawab yang tegas. Semua kode bisa tetap berada dalam satu repository dan satu runtime, namun interaksi antarbagian dibatasi lewat interface internal, bukan akses bebas ke semua tabel atau class.
Pendekatan ini cocok ketika:
- Tim masih kecil atau menengah.
- Produk berubah cepat dan butuh iterasi sederhana.
- Domain belum cukup stabil untuk dipisah menjadi service independen.
- Operasional ingin tetap sederhana.
Kelebihannya:
- Latensi antar-modul sangat rendah karena tidak ada network hop.
- Debugging lebih mudah dibanding banyak service.
- Testing integrasi lebih sederhana.
- Biaya infrastruktur dan operasional lebih rendah.
- Deploy awal lebih cepat karena hanya satu artefak utama.
Kekurangannya:
- Satu memory leak, thread pool habis, atau query berat bisa memengaruhi seluruh aplikasi.
- Sulit melakukan scaling hanya untuk satu fungsi panas, misalnya search atau image processing.
- Batas modul mudah rusak jika disiplin kode lemah.
- Release bisa menjadi berisiko jika banyak domain berubah bersamaan.
Monolit modular sering kali adalah pilihan terbaik lebih lama daripada yang dibayangkan banyak tim, asalkan benar-benar modular, bukan sekadar “semua ada dalam satu folder services”.
2. Service terpisah
Pada model ini, domain tertentu dijalankan sebagai aplikasi atau service independen. Contoh umum: auth, payment, notification, catalog, fulfillment. Tujuan utamanya bukan gaya arsitektur, melainkan isolasi kegagalan, ownership tim, dan scaling spesifik per domain.
Cocok ketika:
- Satu domain punya beban, SLA, atau pola scaling yang sangat berbeda.
- Satu komponen sering bermasalah dan tidak boleh menjatuhkan komponen lain.
- Tim sudah cukup besar untuk memiliki ownership operasional terpisah.
- Dependency eksternal berisiko tinggi perlu dikarantina.
Kelebihannya:
- Blast radius lebih kecil jika service dipisah dengan baik.
- Deploy lebih independen.
- Skalabilitas lebih presisi.
- Teknologi dapat dipilih sesuai kebutuhan domain, meski ini bukan alasan utama.
Kekurangannya:
- Latensi naik karena komunikasi antarlayanan lewat network.
- Failure mode lebih kompleks: timeout, retry storm, partial failure, contract drift.
- Observabilitas lebih sulit karena trace menyebrang banyak hop.
- Biaya operasional naik: CI/CD, monitoring, secret management, on-call, container orchestration, jaringan.
Kesalahan umum adalah memecah service terlalu dini tanpa observabilitas dan batas domain yang jelas. Hasilnya bukan isolasi, melainkan sistem tersebar yang tetap tightly coupled.
3. Event-driven
Event-driven architecture memindahkan sebagian coupling dari request sinkron ke event asinkron. Satu komponen menerbitkan event seperti OrderPlaced, lalu komponen lain merespons sesuai kebutuhan: mengirim email, mengurangi stok, membuat invoice, atau mencatat audit.
Cocok ketika:
- Alur kerja tidak harus selesai dalam satu request.
- Anda ingin mengurangi ketergantungan sinkron antar-komponen.
- Satu kejadian bisnis perlu dipakai banyak konsumen.
- Resiliensi lebih penting daripada konsistensi instan di semua sisi.
Kelebihannya:
- Publisher tidak perlu menunggu semua subscriber selesai.
- Gangguan pada satu consumer tidak harus memblok proses utama.
- Mudah menambah proses turunan tanpa mengubah jalur request inti.
Kekurangannya:
- Debugging lebih sulit karena alur tersebar dan tidak linear.
- Perlu menangani idempotency, duplicate delivery, retry, dan dead-letter queue.
- Konsistensi menjadi eventual, bukan instan.
- Schema event harus dikelola hati-hati agar tidak merusak consumer.
Event-driven sangat membantu membatasi blast radius, tetapi bukan solusi ajaib. Jika semua consumer tetap bergantung pada satu database yang sama, atau publisher tetap menunggu konfirmasi downstream secara sinkron, manfaat isolasinya banyak hilang.
4. Komponen terisolasi
Yang dimaksud di sini bukan selalu microservices penuh. Komponen terisolasi bisa berupa worker terpisah, queue terdedikasi, database read replica, process pool berbeda, bulkhead pada koneksi, atau jalur eksekusi khusus untuk integrasi eksternal yang berisiko.
Ini sering menjadi pilihan paling praktis untuk meningkatkan ketahanan tanpa migrasi arsitektur besar.
Contohnya:
- Proses generate PDF dipindahkan ke worker terpisah agar CPU spike tidak memukul API utama.
- Pengiriman webhook ke partner dilakukan dari queue khusus dengan retry policy sendiri.
- Integrasi payment menggunakan circuit breaker dan timeout agresif agar request checkout tidak menggantung terlalu lama.
- Search dipisah ke service/cluster tersendiri karena pola query dan failure mode berbeda dari transaksi inti.
Pendekatan ini sering memberi rasio manfaat-biaya terbaik karena menargetkan area berisiko tinggi, bukan memecah semua hal sekaligus.
Matriks keputusan: pilih yang mana?
Tabel berikut membantu memilih pendekatan berdasarkan kebutuhan praktis, bukan tren.
| Pilihan | Kapan dipilih | Keuntungan utama | Biaya/Risiko utama |
|---|---|---|---|
| Monolit modular | Tim kecil, domain masih berubah, butuh kecepatan delivery | Sederhana, latensi rendah, operasional murah | Blast radius besar jika batas modul lemah |
| Service terpisah | Domain jelas, scaling berbeda, SLA berbeda, ownership tim terpisah | Isolasi deploy dan failure lebih baik | Latensi, observabilitas, operasional lebih rumit |
| Event-driven | Workflow asinkron, banyak subscriber, proses turunan tidak perlu sinkron | Mengurangi coupling runtime, tahan partial failure | Eventually consistent, debugging dan replay lebih sulit |
| Komponen terisolasi | Ada hotspot risiko spesifik tanpa perlu memecah seluruh sistem | Perbaikan fokus dengan biaya moderat | Bisa jadi tambalan jika akar coupling tidak dibenahi |
Versi ringkas keputusan
- Pilih monolit modular jika masalah utama Anda adalah produktivitas tim dan kompleksitas bisnis belum menuntut pemisahan runtime.
- Pilih service terpisah jika ada domain yang secara nyata perlu diisolasi karena beban, risiko, atau cadence deploy berbeda.
- Pilih event-driven jika proses turunan tidak perlu blocking request utama dan Anda siap mengelola konsistensi eventual.
- Pilih komponen terisolasi jika ingin mengecilkan blast radius pada area spesifik tanpa membayar kompleksitas microservices penuh.
Contoh skenario backend nyata
Skenario 1: Aplikasi e-commerce yang baru tumbuh
Anda punya modul catalog, cart, checkout, payment, notification, dan admin. Tim masih 6 orang. Traffic belum ekstrem, tetapi insiden paling sering datang dari integrasi eksternal: payment dan WhatsApp/email provider.
Pilihan yang masuk akal:
- Mulai dengan monolit modular.
- Checkout tetap sinkron untuk validasi inti.
- Notification dibuat asinkron via queue/event.
- Payment integration dibungkus dengan timeout, retry terbatas, idempotency key, dan fallback status
pending. - Worker notification dipisah dari web process agar lonjakan pengiriman tidak memukul API.
Ini memberi isolasi pada area berisiko tanpa memecah semua domain menjadi service terpisah.
Skenario 2: Sistem SaaS B2B dengan audit dan billing kompleks
Billing memiliki aturan pajak, invoice, dan rekonsiliasi yang berkembang cepat. Audit log harus sangat andal. Sementara modul user management relatif stabil.
Pilihan yang masuk akal:
- Tetap gunakan monolit modular untuk sebagian besar domain.
- Pisahkan billing menjadi service jika release cadence, model data, dan kebutuhan isolasinya sudah jelas berbeda.
- Audit log dibuat append-only dan diproses dengan event agar kegagalan audit consumer tidak memblok transaksi utama.
- Jika billing provider eksternal sering lambat, gunakan queue dan compensation flow, bukan memaksa semua request user menunggu provider itu.
Pemisahan service di sini punya alasan yang nyata: billing memiliki failure mode, kebutuhan scaling, dan risiko operasional berbeda.
Skenario 3: Platform internal dengan batch berat
API utama ringan, tetapi ada proses impor data CSV besar, generate report, dan sinkronisasi ke vendor tiap malam. Selama ini semua berjalan dalam satu aplikasi dan sering menyebabkan timeout atau CPU spike.
Pilihan yang masuk akal:
- Tetap pertahankan aplikasi inti sebagai monolit modular.
- Pindahkan batch job dan report generator ke worker terisolasi.
- Gunakan queue berbeda untuk pekerjaan berat dan ringan.
- Tambahkan rate limit dan concurrency limit pada worker tertentu.
Ini contoh klasik bahwa komponen terisolasi sering lebih efektif daripada microservices penuh.
Dampak pada latensi, biaya operasional, observabilitas, deploy, dan maintainability
Latensi
Monolit modular hampir selalu menang untuk jalur sinkron karena tidak ada serialisasi jaringan, DNS, load balancer, dan timeout antarservice. Service terpisah menambah hop jaringan dan kegagalan baru. Event-driven dapat menurunkan latensi perceived pada user karena request utama menjadi lebih tipis, tetapi total waktu end-to-end bisa lebih lama karena proses dilakukan di belakang layar.
Prinsip praktis:
- Jangan memecah jalur yang sangat sensitif latensi tanpa alasan kuat.
- Gunakan asinkron untuk pekerjaan yang tidak perlu selesai sebelum respons dikembalikan.
- Waspadai fan-out sinkron: satu request memanggil tiga service, lalu masing-masing memanggil dua service lain.
Biaya operasional
Setiap service baru menambah biaya nyata: pipeline deploy, dashboard, alarm, secret, tracing, runbook, autoscaling, kebijakan akses, backup, dan rotasi sertifikat. Secara teori pemisahan terlihat bersih, tetapi secara operasional mahal.
Karena itu, jika tujuan Anda hanya mengisolasi satu dependency bermasalah, worker atau queue terdedikasi sering lebih murah daripada service baru lengkap dengan database sendiri.
Observabilitas
Monolit lebih mudah ditelusuri karena log dan stack trace berada dalam satu tempat. Pada arsitektur service atau event-driven, Anda perlu observabilitas yang lebih disiplin:
- Correlation ID atau trace ID yang dibawa lintas request dan event.
- Structured logging, bukan log string acak.
- Metric per dependency: latency, error rate, queue lag, retry count.
- Alert berbasis gejala penting, bukan semua error kecil.
Tanpa ini, sistem terpisah akan terasa “lebih modular di diagram, lebih gelap di produksi”.
Deploy
Monolit menyederhanakan release, tetapi blast radius deploy lebih besar. Service terpisah memungkinkan deploy independen, namun compatibility antarkontrak harus dijaga. Event-driven menambah kebutuhan versioning schema dan backward compatibility event.
Pedoman aman:
- Kontrak antarsistem harus kompatibel ke belakang selama masa transisi.
- Gunakan feature flag untuk perilaku berisiko tinggi.
- Untuk event, hindari mengubah makna field lama secara diam-diam.
Maintainability
Maintainability tidak identik dengan jumlah service sedikit atau banyak. Yang lebih menentukan adalah:
- Batas domain jelas atau tidak.
- Ada aturan dependensi atau tidak.
- Data ownership tegas atau tidak.
- Perubahan mudah diuji dan diamati atau tidak.
Monolit modular yang disiplin bisa jauh lebih mudah dirawat daripada 20 microservice yang saling bergantung dan berbagi database.
Pola implementasi untuk membatasi blast radius
Gunakan timeout dan bulkhead untuk dependency eksternal
Dependency eksternal adalah “kembang api saat mendarat” dalam dunia software: datang dari luar, tidak bisa Anda kontrol, dan bisa mengenai jalur kritis di momen terburuk. Karena itu, jangan biarkan koneksi ke provider eksternal memakai timeout longgar atau berbagi pool tanpa batas.
// Contoh pseudocode untuk call ke provider eksternal
result = withTimeout(2_seconds, () => paymentProvider.charge(request))
.withCircuitBreaker("payment-provider")
.withRetry(maxAttempts=2, retryOn=[timeout, transient_error]);
if (result.failedTemporarily()) {
markPaymentAsPending(orderId);
publishEvent("PaymentPending", { orderId });
}Poin pentingnya bukan sintaks, melainkan prinsipnya: batasi waktu tunggu, batasi retry, dan siapkan status transisional agar sistem inti tetap hidup.
Pisahkan queue berdasarkan karakteristik beban
Jangan campur email, webhook vendor, image processing, dan pekerjaan kritis dalam satu queue tanpa prioritas. Satu jenis job yang macet dapat menahan yang lain.
queues:
critical:
purpose: checkout follow-up, stok, invoice
concurrency: rendah-menengah, latency-sensitive
integrations:
purpose: webhook partner, sinkronisasi vendor
concurrency: dibatasi, retry agresif dengan DLQ
heavy:
purpose: PDF, report, impor besar
concurrency: rendah, resource-intensiveIni adalah bentuk isolasi sederhana yang sering sangat efektif.
Buat batas modul yang nyata meski masih monolit
Jika tetap memakai monolit, cegah coupling liar sejak awal:
- Setiap modul punya service/interface publik sendiri.
- Batasi akses langsung ke tabel domain lain.
- Pisahkan package/folder berdasarkan domain, bukan tipe teknis semata.
- Tambahkan tes integrasi untuk kontrak internal penting.
Tujuannya agar saat suatu hari perlu dipisah menjadi service, Anda tidak harus membedah spaghetti dependency terlebih dahulu.
Tanda bahwa Anda sedang overengineering
- Memecah service karena “nanti pasti scale”, padahal bottleneck nyata belum ada.
- Membuat event untuk semua hal, termasuk alur yang jelas-jelas perlu hasil sinkron instan.
- Menambah broker, service mesh, saga orchestration, dan banyak pipeline sebelum tim mampu mengoperasikannya.
- Setiap domain kecil punya database sendiri, tetapi reporting dan join bisnis tetap dilakukan lintas semuanya dengan cara manual yang menyakitkan.
- Incident review menunjukkan mayoritas masalah berasal dari kompleksitas distribusi, bukan dari kebutuhan bisnis.
Jika tim lebih sering memperbaiki plumbing arsitektur daripada menyelesaikan masalah produk, itu tanda desain sudah terlalu mahal untuk konteks sekarang.
Checklist evaluasi untuk tim kecil
- Apakah insiden utama berasal dari dependency eksternal atau dari kode internal? Jika eksternal, fokuslah pada timeout, retry, queue, dan isolasi worker terlebih dahulu.
- Apakah satu domain benar-benar perlu scaling terpisah? Jika belum, jangan pecah service hanya karena asumsi.
- Apakah tim punya observabilitas memadai? Jika belum ada trace ID, metric queue, dan alert dependency, arsitektur terdistribusi akan sulit dioperasikan.
- Apakah alur ini harus sinkron? Jika tidak, jadikan asinkron.
- Apakah batas modul di monolit sudah jelas? Jika belum, perbaiki modularitas dulu sebelum bicara microservices.
- Apakah on-call capacity cukup? Banyak service berarti lebih banyak sumber insiden.
Checklist evaluasi untuk tim yang sedang tumbuh
- Domain mana yang punya SLA, compliance, atau release cadence berbeda? Itu kandidat kuat untuk dipisah.
- Apakah satu deploy sering menyentuh terlalu banyak area? Jika ya, coupling deploy mungkin sudah terlalu tinggi.
- Apakah database bersama menjadi sumber lock, migrasi berisiko, atau query silang antardomain? Ini sinyal bahwa pemisahan ownership data perlu dipikirkan.
- Apakah antrean pekerjaan dari satu domain mengganggu domain lain? Jika iya, isolasi queue atau worker dulu.
- Apakah consumer event dapat gagal tanpa memblok publisher? Jika tidak, event Anda mungkin hanya sinkron terselubung.
- Apakah tim punya kemampuan contract testing, tracing, dan incident response lintas layanan? Jika belum, batasi jumlah service independen.
Kesimpulan
Merancang sistem tahan insiden berarti menerima bahwa gangguan tak terduga akan terjadi, termasuk dari luar area kendali Anda. Seperti analogi pesawat yang terkena kembang api saat mendarat, masalah serius bisa datang dari faktor eksternal pada waktu yang paling buruk. Arsitektur yang baik adalah yang membatasi penyebaran dampaknya.
Untuk banyak tim, jawaban awal terbaik adalah monolit modular dengan isolasi terarah: queue terpisah, worker khusus, timeout yang ketat, dan event hanya untuk alur yang memang layak asinkron. Pindah ke service terpisah ketika ada alasan yang terukur: domain stabil, risiko berbeda, scaling berbeda, atau ownership tim sudah jelas. Dengan pendekatan itu, Anda tidak sekadar membangun sistem yang “bisa jalan”, tetapi sistem yang tetap waras saat insiden benar-benar terjadi.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!