Memilih monolit vs service dengan prinsip fairness beban pada dasarnya adalah soal membagi sumber daya secara adil ketika banyak fitur, tenant, job, dan tim berbagi sistem yang sama. Jika satu alur kerja bisa menghabiskan CPU, koneksi database, partisi queue, atau bandwidth observability sampai alur lain ikut melambat, masalah Anda bukan sekadar “arsitektur kurang modern”, melainkan fairness yang buruk.
Karena itu, keputusan antara modular monolith, service terpisah, atau arsitektur event-driven sebaiknya dimulai dari pertanyaan praktis: beban siapa yang harus diisolasi, sumber daya apa yang paling sering diperebutkan, dan seberapa mahal dampaknya jika satu komponen bersikap seperti noisy neighbor? Dari situ, pilihan arsitektur menjadi lebih rasional dan lebih mudah dioperasikan.
Apa yang dimaksud fairness beban dalam arsitektur backend?
Dalam konteks sistem backend, fairness beban berarti sumber daya bersama dibagi dengan cara yang tidak membiarkan satu workload mendominasi sampai workload lain kehilangan layanan yang layak. Sumber daya yang dimaksud bukan hanya CPU dan memori, tetapi juga:
- Koneksi database dan waktu query
- Thread pool atau worker pool
- Queue throughput dan prioritas job
- Cache space dan bandwidth cache
- Rate limit API upstream/downstream
- Network egress dan latency budget
- Human bandwidth: deployment, review, ownership, incident response
Fairness tidak selalu berarti semua mendapat porsi sama besar. Dalam praktik, fairness sering berarti alokasi yang proporsional dan terkontrol. Misalnya, proses ekspor laporan boleh memakan resource besar, tetapi tidak boleh membuat checkout gagal. Tenant enterprise boleh punya limit lebih tinggi, tetapi tidak boleh mendorong tenant kecil ke timeout terus-menerus.
Inti praktisnya: arsitektur yang baik bukan yang paling terdistribusi, melainkan yang paling jelas dalam membatasi dampak satu beban terhadap beban lain.
Mengapa fairness relevan saat memilih monolit, service terpisah, dan event-driven?
Banyak diskusi arsitektur berhenti di “monolit lebih sederhana” atau “microservices lebih scalable”. Itu terlalu umum. Yang lebih menentukan adalah di mana kontensi terjadi dan apakah kontensi itu bisa dikendalikan tanpa memecah sistem terlalu cepat.
Monolit modular
Monolit modular cocok jika mayoritas workload masih berbagi domain data dan profil beban belum sangat bertolak belakang. Keuntungannya:
- Satu proses deploy dan observability lebih sederhana
- Transaksi lintas modul lebih mudah
- Refactoring antar modul lebih murah
- Biaya infrastruktur biasanya lebih rendah
Namun fairness di monolit mudah rusak jika semua fitur berbagi hal yang sama tanpa pembatas: connection pool, queue worker, thread pool, scheduler, bahkan log pipeline. Contoh klasiknya:
- Job sinkronisasi besar menghabiskan koneksi DB sehingga API interaktif melambat
- Endpoint laporan menjalankan query berat yang mendorong query transaksi ke antrean
- Satu tenant sangat aktif memenuhi cache dengan objek-objek besar
Artinya, monolit bukan masalah jika Anda masih bisa menegakkan isolasi beban di dalam satu sistem.
Service terpisah
Memisahkan service masuk akal ketika Anda butuh batas kapasitas dan kegagalan yang jelas. Service terpisah membantu fairness karena:
- Setiap service bisa punya autoscaling sendiri
- Satu service yang boros CPU tidak otomatis mengambil CPU service lain
- Connection pool, cache, dan queue bisa dipisah
- Blast radius kegagalan lebih kecil jika desain dependency rapi
Namun service terpisah bukan fairness gratis. Anda memindahkan masalah dari kontensi internal menjadi kontensi jaringan dan koordinasi:
- Peningkatan latensi antar service
- Retry storm saat dependency lambat
- Cascading failure jika timeout dan circuit breaker tidak disiplin
- Biaya observability dan debugging meningkat tajam
Jika pemisahan service tidak dibarengi dengan kebijakan resource dan dependency yang sehat, noisy neighbor tetap ada, hanya lokasinya berbeda.
Event-driven
Arsitektur event-driven berguna ketika beban perlu dipisah secara asinkron, throughput tinggi, atau proses bisnis memang toleran terhadap eventual consistency. Dari sisi fairness, pendekatan ini memberi alat yang kuat:
- Producer dan consumer terisolasi waktu eksekusinya
- Backpressure lebih mudah diterapkan lewat queue/topic
- Consumer berbeda bisa diberi kapasitas berbeda
- Workload berat bisa diproses terpisah dari jalur request user
Trade-off-nya juga nyata:
- Debugging lebih sulit karena alur menyebar
- Ordering, deduplication, dan idempotency harus dirancang
- Lag pada queue bisa menyamarkan overload sampai terlambat
- Dead-letter queue sering menumpuk tanpa ownership jelas
Event-driven sangat membantu fairness bila Anda memang perlu memisahkan jalur beban. Tetapi jika semua hal dipaksa jadi event hanya demi “loose coupling”, kompleksitas operasional bisa lebih mahal daripada manfaatnya.
Cara mengevaluasi arsitektur dengan lensa fairness
Daripada mulai dari pola arsitektur, mulailah dari inventaris kontensi. Tanyakan secara konkret:
- Apa workload utama sistem? Request interaktif, batch, sinkronisasi, ekspor, indexing, ML inference, billing, webhook?
- Sumber daya mana yang diperebutkan? CPU, memori, DB pool, disk IOPS, network, queue workers?
- Siapa yang paling berisiko menjadi noisy neighbor? Satu tenant besar, satu modul laporan, satu cron harian?
- Apakah beban itu bisa dibatasi di dalam monolit? Pool terpisah, queue terpisah, rate limit, scheduler, partitioning?
- Kalau gagal, siapa yang ikut terdampak? Hanya fitur itu sendiri, atau seluruh transaksi utama?
- Apakah tim butuh siklus deploy terpisah? Fairness operasional sama pentingnya dengan fairness komputasi.
Dari jawaban ini, Anda bisa membedakan tiga jenis masalah:
- Kontensi yang bisa dikendalikan secara lokal → sering masih cocok di monolit modular
- Kontensi yang perlu batas resource keras → service terpisah mulai layak
- Kontensi yang sifatnya bursty dan asinkron → queue/event-driven biasanya lebih efektif
Matriks keputusan: monolit modular vs service terpisah vs event-driven
| Aspek | Monolit Modular | Service Terpisah | Event-Driven |
|---|---|---|---|
| Fairness CPU/memori | Sulit jika semua dalam proses/pool yang sama, tapi bisa dibantu limit internal | Lebih baik karena batas resource per service jelas | Baik untuk workload async, tergantung kapasitas consumer |
| Fairness koneksi database | Sering jadi titik kontensi utama | Bisa dipisah per service atau per datastore | Bisa mengurangi tekanan request sync, tapi consumer tetap perlu kontrol |
| Noisy neighbor | Mudah terjadi jika queue, pool, dan cache bersama | Lebih terisolasi, tetapi bisa pindah ke dependency chain | Bisa ditahan lewat topic/consumer group, tetapi lag perlu diawasi |
| Isolasi kegagalan | Terbatas, terutama jika proses tunggal dan dependency bersama | Lebih baik bila dependency dan fallback dirancang baik | Baik untuk decoupling waktu, buruk jika event pipeline jadi bottleneck tunggal |
| Observability | Paling mudah | Lebih sulit: tracing, timeout, retry, dependency map | Paling sulit: causality dan lag harus terlihat |
| Biaya cloud | Biasanya paling rendah | Naik karena runtime, network, observability, idle capacity | Bisa efisien untuk burst, tapi broker dan operasional menambah biaya |
| Maintainability | Baik jika modular discipline kuat | Baik jika boundary jelas; buruk jika terlalu granular | Baik untuk pipeline tertentu; buruk jika dipaksakan ke semua alur |
| Kecepatan tim kecil | Sangat baik | Sering melambat di awal | Baik hanya untuk kasus async yang jelas |
Jika harus diringkas: mulai dari monolit modular, tambahkan fairness control internal, lalu pecah service hanya pada titik kontensi yang benar-benar terbukti.
Teknik fairness yang sering cukup tanpa memecah service
Sering kali akar masalah bukan “monolit”, melainkan absennya pembatasan internal. Berikut langkah yang biasanya lebih murah daripada memecah service terlalu dini.
1. Pisahkan jalur interaktif dan batch
Jangan biarkan request user dan job background berbagi pool tanpa batas. Minimal pisahkan:
- Queue berbeda untuk prioritas tinggi dan rendah
- Worker count berbeda per queue
- Connection pool DB terpisah bila memungkinkan
- Timeout berbeda untuk endpoint user-facing vs batch
# Contoh konseptual worker terpisah berdasarkan kelas beban
workers:
api-critical:
queue: [checkout, auth, billing]
concurrency: 8
background-default:
queue: [email, sync, webhook]
concurrency: 4
batch-heavy:
queue: [report-export, reindex]
concurrency: 2Tujuannya bukan sekadar prioritas, tetapi mencegah queue berat menelan seluruh worker.
2. Terapkan rate limit dan concurrency limit per tenant atau per operasi
Jika sistem Anda multi-tenant, fairness hampir selalu membutuhkan limit per tenant. Tanpa itu, satu pelanggan besar dapat menghabiskan resource bersama. Polanya bisa berupa:
- Rate limit per API key atau tenant
- Concurrency cap untuk job mahal seperti export/import
- Quota harian untuk operasi biaya tinggi
// Pseudocode middleware fairness per tenant
function allowRequest(tenantId, operation) {
const limit = getConfiguredLimit(tenantId, operation)
const inFlight = getInFlightCount(tenantId, operation)
if (inFlight >= limit.concurrent) {
return reject(429, "too many concurrent operations")
}
return accept()
}Limit seperti ini sering memberi hasil lebih cepat daripada memecah service, karena akar masalahnya adalah alokasi, bukan struktur kode.
3. Klasifikasikan query dan pool database
Database adalah sumber unfairness paling umum. Satu query laporan atau full scan dapat merusak latency fitur transaksi. Langkah praktis:
- Pisahkan query OLTP dan query analitik jika pola beban berbeda jauh
- Gunakan replica baca untuk laporan jika konsistensi ketat tidak dibutuhkan
- Batasi timeout query mahal
- Audit endpoint yang melakukan N+1 query atau scan besar
Jika problem utamanya ada di DB yang sama, memecah service di layer aplikasi belum tentu menolong.
4. Gunakan backpressure yang eksplisit
Sistem yang adil tahu kapan harus menolak atau menunda beban. Tanda sistem tanpa backpressure:
- Semua request diterima lalu timeout belakangan
- Queue tumbuh tanpa batas
- Retry otomatis memperparah overload
Praktik yang lebih sehat:
- 429 atau 503 yang jelas saat kapasitas habis
- Bounded queue
- Retry dengan jitter dan batas maksimum
- Circuit breaker untuk dependency yang lambat
5. Pecah modul secara logis sebelum pecah deploy unit
Banyak tim melompat ke microservices karena struktur monolit kacau. Biasanya langkah lebih aman adalah:
- Buat boundary modul yang jelas
- Tetapkan API internal antar modul
- Pisahkan ownership modul
- Ukur beban per modul
Dengan begitu, saat satu modul benar-benar perlu dipisah menjadi service, batasnya sudah nyata dan migrasinya tidak serampangan.
Kapan service benar-benar perlu dipisah?
Gunakan pemisahan service bila fairness internal mulai mentok, bukan sekadar karena ukuran codebase bertambah. Sinyal yang layak diperhatikan:
1. Profil beban sangat berbeda dan saling mengganggu
Contoh: API transaksi butuh latency rendah dan burst pendek, sementara engine rekomendasi butuh CPU tinggi dan komputasi panjang. Jika limit internal sudah dicoba tetapi tetap saling mengganggu, service terpisah masuk akal.
2. Kebutuhan kapasitas per komponen berbeda jauh
Misalnya modul webhook butuh banyak koneksi keluar dan autoscale agresif, sedangkan billing butuh stabilitas dan perubahan lambat. Jika satu komponen harus diskalakan 10x lebih sering daripada yang lain, pemisahan bisa mengurangi pemborosan kapasitas.
3. Blast radius kegagalan terlalu besar
Bila bug atau lonjakan pada satu area terus menjatuhkan fitur kritis lain meski queue dan pool sudah dipisah, service boundary dapat menjadi pagar keselamatan.
4. Dependency eksternal sangat spesifik
Jika satu modul sangat terikat pada runtime, library, SLA, atau secret tertentu, pemisahan service dapat menurunkan kompleksitas deployment dan membatasi dampak keamanan maupun insiden.
5. Ownership tim memang sudah berbeda dan stabil
Fairness juga soal aliran kerja manusia. Jika tim A dan tim B terus saling menunggu dalam deploy, review, dan incident handling, pemisahan service mungkin memberi batas ownership yang lebih sehat. Tapi ini efektif hanya jika boundary domain jelas; jika tidak, Anda hanya memindahkan coupling dari kode ke rapat koordinasi.
Tiga skenario nyata
1. Startup kecil: satu produk, satu tim backend, trafik belum ekstrem
Situasi: Tim 4-8 engineer, satu aplikasi utama, fitur inti adalah auth, billing, dashboard, dan export CSV. Insiden utama: export besar memperlambat API.
Pilihan yang sehat: tetap di modular monolith.
Kenapa:
- Biaya koordinasi service terpisah terlalu mahal untuk tim kecil
- Masalah fairness bisa diatasi lebih murah dengan queue dan pool terpisah
- Observability satu aplikasi lebih sederhana saat tim operasional masih kecil
Langkah implementasi:
- Pisahkan export ke queue prioritas rendah
- Batasi satu export aktif per tenant
- Gunakan replica baca untuk laporan jika perlu
- Tambahkan dashboard DB pool saturation, queue lag, dan p95 latency endpoint inti
Hindari: memecah “report-service” hanya karena satu fitur berat, padahal bottleneck sebenarnya adalah query dan worker bersama.
2. SaaS tumbuh: multi-tenant, tenant besar mulai mendominasi
Situasi: Produk B2B dengan banyak integrasi, webhook, sinkronisasi katalog, audit log, dan job berkala. Satu tenant enterprise bisa menghasilkan beban puluhan kali tenant kecil.
Pilihan yang sehat: kombinasi modular monolith + beberapa service terpisah untuk workload yang memang berbeda profilnya.
Kandidat yang sering layak dipisah:
- Ingestion webhook ber-volume tinggi
- Pipeline sinkronisasi batch
- Search indexing atau analytics
Kenapa:
- Workload bursty dan async butuh autoscaling sendiri
- Noisy neighbor antar tenant sulit diatasi jika semua berbagi worker yang sama
- Komponen async biasanya lebih mudah dipisah daripada transaksi inti
Langkah implementasi:
- Terapkan rate limit per tenant pada webhook masuk
- Partisi queue/topic per kelas tenant atau jenis event
- Pisahkan jalur transaksi inti tetap di monolit
- Gunakan idempotency key untuk consumer
Hindari: memecah service inti transaksi terlalu cepat jika domain data masih sangat saling bergantung.
3. Enterprise: banyak domain, banyak tim, compliance dan SLA berbeda
Situasi: Banyak bounded context, tim independen, kebutuhan audit, data retention, dan SLA antar domain berbeda. Beberapa layanan harus sangat stabil, yang lain bisa eventual consistency.
Pilihan yang sehat: service terpisah untuk domain dengan kebutuhan operasi dan kepatuhan berbeda, dipadukan dengan event-driven untuk integrasi antar domain.
Kenapa:
- Fairness tim dan fairness kapasitas sama-sama penting
- Domain dengan SLA tinggi perlu resource dan release policy sendiri
- Event-driven membantu mengurangi coupling sinkron antar organisasi besar
Langkah implementasi:
- Tentukan SLO per service dan budget error
- Pasang timeout, retry budget, dan circuit breaker lintas service
- Tetapkan ownership jelas untuk schema event, DLQ, dan replay
- Pisahkan observability per domain, tetapi pertahankan trace lintas sistem
Hindari: microservices sangat granular tanpa platform dan standar operasional matang. Tanpa itu, fairness malah runtuh karena semua tim berebut kapasitas platform, pipeline CI, dan perhatian SRE.
Observability untuk mengukur fairness, bukan sekadar uptime
Jika Anda ingin memakai fairness sebagai dasar keputusan arsitektur, metriknya harus bisa menunjukkan siapa mengganggu siapa. Metrik generik seperti CPU total atau error rate total sering tidak cukup. Yang lebih berguna:
Metrik utama
- Latency per endpoint/per operasi/per tenant
- Queue lag, queue depth, dan age of oldest message
- Connection pool saturation
- DB query latency menurut kelas query
- In-flight request/job per tenant
- Throttle/reject rate
- Retry rate dan timeout rate antar dependency
Pertanyaan diagnostik yang membantu
- Apakah p95 tenant kecil memburuk saat tenant besar melakukan import?
- Apakah checkout melambat saat job export aktif?
- Apakah queue prioritas rendah bisa membuat queue penting menunggu scheduler?
- Apakah satu service downstream menyebabkan retry storm di banyak caller?
Tanpa dimensi tenant, operasi, dan prioritas, tim sering salah menyimpulkan bahwa “sistem baik-baik saja secara rata-rata” padahal unfairness sudah menggerus sebagian pengguna.
Biaya cloud: fairness yang buruk sering terlihat sebagai pemborosan
Arsitektur service terpisah sering dipilih atas nama scalability, tetapi biaya cloud bisa melonjak jika boundary tidak tepat. Beberapa pola umum:
- Idle capacity berlebih: banyak service kecil masing-masing butuh headroom sendiri
- Biaya network dan observability: panggilan antar service, tracing, log, metric cardinality
- Overprovisioning karena dependency sync: caller harus diskalakan untuk menutupi latency callee
Di sisi lain, monolit yang fairness-nya buruk juga mahal:
- Anda menambah instance seluruh aplikasi padahal hanya satu modul yang berat
- Cache dan DB dipaksa naik kelas karena noisy neighbor
- Incident lebih sering dan waktu mitigasi lebih lama
Jadi pertanyaannya bukan “mana lebih murah secara absolut”, melainkan apakah unit skalanya cocok dengan unit bebannya.
Anti-pattern umum
1. Memecah service untuk menghindari refactoring modul
Jika batas domain belum jelas, service hanya menyembunyikan coupling di balik HTTP atau event. Fairness tidak membaik karena sumber kontensinya masih sama.
2. Semua workload masuk satu queue karena “lebih simpel”
Ini resep klasik noisy neighbor. Queue tunggal tanpa prioritas dan concurrency cap membuat job murah ikut menunggu job mahal.
3. Event-driven tanpa idempotency dan backpressure
Begitu consumer lambat lalu retry meledak, unfairness muncul dalam bentuk lag, duplikasi, dan DLQ yang membusuk.
4. Service terlalu granular
Jika satu use case sederhana melewati lima service sinkron, fairness runtuh melalui timeout, retry, dan koordinasi deploy. Granularitas yang salah membuat setiap request membawa overhead operasional.
5. Mengukur rata-rata, bukan distribusi
Rata-rata latency yang baik bisa menyamarkan tenant tertentu atau operasi tertentu yang selalu kalah berebut resource.
6. Menganggap isolasi proses otomatis berarti isolasi database
Banyak tim memecah service, tetapi semua tetap menembak database yang sama tanpa guardrail. Akibatnya kontensi inti tidak hilang.
Panduan keputusan yang bisa dipakai minggu ini
- Petakan workload: tandai mana yang interaktif, batch, bursty, dan mahal.
- Daftar sumber daya bersama: DB pool, queue, cache, CPU, dependency eksternal.
- Ukur unfairness: cari tenant, endpoint, atau job yang paling sering menyebabkan degradasi lintas fitur.
- Pasang guardrail internal dulu: rate limit, queue terpisah, timeout, pool, bounded concurrency.
- Evaluasi ulang: jika fairness membaik, tetap di monolit modular.
- Pisahkan hanya unit yang punya profil beban, scaling, atau SLA berbeda nyata.
- Untuk beban async, pertimbangkan event-driven dengan ownership jelas atas retry, DLQ, dan replay.
Urutan ini penting karena arsitektur yang lebih terdistribusi tidak otomatis lebih adil. Sering kali, fairness yang buruk berasal dari absennya kebijakan kapasitas, bukan dari bentuk deploy unit.
Kesimpulan
Memilih monolit vs service dengan prinsip fairness beban membantu Anda menghindari dua kesalahan umum: bertahan terlalu lama pada monolit yang membiarkan noisy neighbor merusak sistem, atau memecah service terlalu cepat sehingga kompleksitas operasional melonjak tanpa menyelesaikan kontensi inti.
Untuk banyak tim, jawaban terbaik adalah monolit modular yang disiplin, ditambah pemisahan queue, concurrency limit, rate limit per tenant, dan observability yang menunjukkan unfairness secara nyata. Service terpisah layak dipilih ketika ada kebutuhan isolasi resource, scaling, atau blast radius yang tidak lagi bisa dikendalikan secara internal. Event-driven paling berguna ketika Anda perlu memisahkan waktu dan throughput beban async, bukan sebagai default untuk semua alur.
Jika Anda ragu, jangan mulai dari pertanyaan “arsitektur mana yang lebih modern?”. Mulailah dari “beban siapa yang sedang menang sendiri, dan mekanisme apa yang paling murah untuk menghentikannya?”
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!