Monolith modular vs serverless bukan soal mana yang lebih modern, tetapi mana yang memberi biaya total lebih rendah dan maintainability lebih baik untuk konteks produk Anda. Untuk produk web/backend skala kecil hingga menengah, keputusan yang salah biasanya tidak langsung terlihat di tagihan cloud, melainkan muncul sebagai deploy yang rapuh, debugging yang lambat, log yang tercecer, dan tim yang makin sulit bergerak.

Jawaban singkatnya: monolith modular biasanya lebih tepat saat domain bisnis masih sering berubah, tim kecil butuh alur development sederhana, dan Anda ingin testing, deployment, serta debugging yang mudah. Serverless masuk akal saat beban kerja event-driven, traffic fluktuatif, kebutuhan scale tidak merata, atau ada pekerjaan asinkron yang tidak efisien jika dijalankan terus-menerus di server yang selalu aktif. Dalam banyak kasus, pendekatan terbaik justru kombinasi keduanya: aplikasi utama tetap monolith modular, sementara tugas tertentu dipindahkan ke fungsi serverless.

Apa yang dibandingkan: bukan pola organisasi tim, tetapi biaya perubahan sistem

Di artikel ini, fokusnya bukan debat umum arsitektur terdistribusi, melainkan trade-off operasional dan teknis yang akan terasa sehari-hari bagi engineer backend dan full-stack:

  • Kompleksitas pengembangan: seberapa sulit menambah fitur tanpa merusak fitur lain.
  • Latency dan cold start: seberapa konsisten response time untuk request pengguna.
  • Debugging dan observabilitas: seberapa cepat menemukan akar masalah saat error terjadi.
  • Biaya infrastruktur: compute, network, idle capacity, dan komponen pendukung.
  • Biaya operasional tim: CI/CD, review, local development, on-call, dan cognitive load.
  • Deployment, testing, dan maintainability jangka panjang.
  • Vendor lock-in: seberapa mahal migrasi jika kebutuhan berubah.

Monolith modular: sederhana di deployment, kompleksitas ditahan di dalam proses yang sama

Monolith modular adalah satu aplikasi yang dideploy sebagai satu unit, tetapi kode dipecah menjadi modul atau bounded context yang jelas. Semua modul berjalan dalam proses atau runtime yang sama, biasanya berbagi database atau setidaknya berbagi koneksi dan mekanisme transaksi yang sama.

Karakteristik yang membuatnya menarik

  • Satu repositori, satu pipeline utama, satu artefak deploy.
  • Call antar modul murah karena cukup pemanggilan fungsi atau service internal, bukan network hop.
  • Debugging lebih mudah karena stack trace, log, dan state aplikasi lebih terkonsolidasi.
  • Testing integrasi lebih lurus karena dependency tidak perlu diemulasikan lewat jaringan untuk semua kasus.

Risiko yang harus dikendalikan

  • Monolith tanpa disiplin modular cepat berubah menjadi big ball of mud.
  • Shared database memudahkan query lintas modul, tetapi juga membuka pintu coupling tersembunyi.
  • Skala horizontal tetap mungkin, tetapi scaling terjadi untuk seluruh aplikasi, bukan hanya bagian yang panas.

Ciri implementasi yang sehat

Monolith modular yang sehat bukan sekadar folder besar dengan nama modul. Ia butuh batas yang dipatuhi:

  • Setiap modul punya service layer dan aturan akses data sendiri.
  • Modul lain tidak langsung menyentuh tabel internal modul lain tanpa kontrak yang jelas.
  • Komunikasi internal dilakukan melalui interface/service, bukan saling mengimpor utilitas sembarangan.
  • Background job dipisahkan untuk pekerjaan berat agar request web tetap ringan.
// Contoh pseudo-code: modul Order tidak langsung memodifikasi tabel Payment
// tetapi memanggil service kontrak internal.

class CheckoutService {
  constructor(orderService, paymentService, inventoryService) {
    this.orderService = orderService;
    this.paymentService = paymentService;
    this.inventoryService = inventoryService;
  }

  async checkout(input, user) {
    await this.inventoryService.reserve(input.items);
    const order = await this.orderService.createDraft(input, user.id);
    const payment = await this.paymentService.createPaymentIntent(order.id, order.total);
    return { orderId: order.id, paymentToken: payment.token };
  }
}

Contoh di atas sederhana, tetapi penting: batas modul dijaga lewat service kontrak, bukan akses bebas ke semua resource.

Serverless: biaya idle rendah, tetapi kompleksitas berpindah ke orkestrasi dan observabilitas

Serverless di sini berarti fungsi atau komponen komputasi yang dijalankan saat ada request atau event, tanpa Anda mengelola server aplikasi yang selalu aktif. Anda tetap harus memikirkan runtime, timeout, retry, concurrency, koneksi database, IAM, dan tracing; yang hilang adalah pengelolaan server secara langsung, bukan kompleksitas sistem.

Kapan serverless terlihat sangat efisien

  • Traffic tidak stabil: ada lonjakan sesekali, tetapi banyak periode idle.
  • Workload event-driven: webhook, pemrosesan file, thumbnail image, sinkronisasi data, notifikasi, ETL ringan.
  • Scale per fungsi: hanya endpoint atau worker tertentu yang perlu diskalakan agresif.
  • Time-to-market cepat untuk fitur terisolasi yang tidak butuh banyak state lokal.

Biaya tersembunyi yang sering terlambat dihitung

  • Cold start dapat menambah latency pada request sporadis atau fungsi dengan dependency berat.
  • Debugging lebih sulit karena satu alur bisnis bisa menyebar ke banyak function, queue, event, dan layanan cloud.
  • Observabilitas wajib dirancang sejak awal; tanpa correlation ID dan tracing, root cause analysis akan lambat.
  • Biaya jaringan dan komponen pendukung dapat tumbuh: API gateway, log, queue, object storage, secrets, NAT, egress, tracing.
  • Vendor lock-in meningkat jika terlalu banyak menggunakan event format, IAM policy, dan integrasi proprietary.

Kondisi yang sering mengecewakan di produksi

Masalah umum bukan karena function gagal berjalan, tetapi karena pola kerjanya tidak cocok:

  • Function sinkron dipakai untuk operasi yang butuh koneksi database lama atau query berat.
  • Semua endpoint dipindahkan ke serverless padahal sebagian besar traffic stabil dan bisa ditangani lebih murah oleh satu service biasa.
  • Retry otomatis menyebabkan duplikasi efek samping karena operasi tidak idempotent.
  • Log terlalu tersebar sehingga incident response menjadi lambat.
// Contoh pseudo-code untuk handler serverless dengan correlation ID dan idempotency key
export async function handler(event) {
  const correlationId = event.headers?.['x-correlation-id'] || crypto.randomUUID();
  const idempotencyKey = event.headers?.['idempotency-key'];

  logger.info({ correlationId }, 'incoming request');

  if (!idempotencyKey) {
    return { statusCode: 400, body: 'Missing idempotency key' };
  }

  const existing = await idempotencyStore.find(idempotencyKey);
  if (existing) {
    return { statusCode: 200, body: JSON.stringify(existing.response) };
  }

  const result = await applicationService.process(event.body, { correlationId });
  await idempotencyStore.save(idempotencyKey, result);

  return { statusCode: 200, body: JSON.stringify(result) };
}

Poin pentingnya bukan framework-nya, tetapi pola operasional: correlation ID, idempotency, dan logging terstruktur hampir wajib pada sistem serverless yang nyata.

Tabel perbandingan: monolith modular vs serverless

AspekMonolith ModularServerless
Kompleksitas pengembangan awalLebih rendah untuk tim kecil; alur lokal dan debugging lebih sederhanaLebih tinggi jika banyak function, event, permission, dan resource cloud
Latency request sinkronBiasanya lebih konsisten; tidak ada cold start antar modulBisa baik, tetapi sensitif terhadap cold start dan network hop tambahan
Cold startTidak relevan dalam bentuk yang samaPerlu dipertimbangkan terutama pada traffic sporadis
DebuggingRelatif mudah; stack trace dan log lebih terpusatLebih sulit; perlu tracing lintas fungsi dan event
ObservabilitasLebih mudah dimulai, tetap perlu standar loggingHarus dirancang sejak awal agar tidak buta saat incident
Biaya infrastruktur saat idleAda biaya server aktif terus-menerusBiasanya lebih efisien untuk workload yang jarang aktif
Biaya operasional timSering lebih rendah untuk tim kecil-menengahBisa lebih tinggi jika banyak komponen cloud dan alur async
DeploymentSatu artefak; sederhana tetapi blast radius lebih besarGranular; deploy per fungsi, tetapi pipeline bisa lebih rumit
TestingUnit dan integration test lebih mudah ditataButuh strategi contract test, event simulation, dan test environment lebih teliti
Vendor lock-inBiasanya lebih rendahBisa tinggi jika memakai integrasi proprietary secara mendalam
Maintainability jangka panjangBaik jika modularitas disiplin dijagaBaik untuk fungsi terisolasi; buruk jika domain tersebar tanpa batas jelas
Pola workload yang cocokCRUD bisnis inti, dashboard admin, API utama, domain yang sering berubahWebhook, job async, pemrosesan file, scheduler, burst traffic, integrasi event-driven

Trade-off biaya: jangan hanya lihat tagihan compute

Biaya infrastruktur nyata

Pada monolith modular, biaya paling terlihat biasanya compute yang selalu aktif, database, cache, storage, dan monitoring. Ini terasa mahal saat traffic rendah, tetapi perilakunya lebih mudah diprediksi.

Pada serverless, biaya compute per request memang bisa efisien, tetapi jangan hitung compute saja. Total biaya sering dipengaruhi oleh:

  • API gateway atau load balancer.
  • Queue dan event bus.
  • Log dan tracing yang volumenya besar.
  • Koneksi database melalui komponen perantara.
  • Egress atau lalu lintas antar layanan.
  • Retry yang memperbanyak eksekusi.

Aturan praktis: jika arsitektur serverless membuat Anda menambah terlalu banyak layanan pendukung hanya untuk meniru alur aplikasi biasa, kemungkinan besar biaya total tidak serendah yang terlihat di awal.

Biaya operasional tim sering lebih mahal daripada server

Untuk produk kecil hingga menengah, selisih biaya cloud kadang lebih kecil daripada biaya engineer yang habis untuk mengelola kompleksitas. Beberapa contoh:

  • Incident butuh 3 jam karena trace request tersebar di 6 function dan 2 queue.
  • Fitur sederhana butuh perubahan di IAM, schema event, function, dashboard monitoring, dan pipeline deploy.
  • Environment lokal sulit direplikasi, sehingga bug baru terlihat setelah deploy.

Jika tim Anda kecil dan domain bisnis berubah cepat, biaya perubahan seperti ini bisa lebih mahal daripada menjalankan satu service aplikasi yang stabil.

Maintainability jangka panjang: mana yang lebih tahan terhadap perubahan bisnis?

Monolith modular lebih tahan saat domain sering berubah

Produk tahap awal sampai menengah biasanya lebih sering mengubah aturan bisnis daripada mengubah kebutuhan scale ekstrem. Dalam situasi ini, monolith modular unggul karena:

  • Refactor lintas modul lebih mudah dilakukan dalam satu codebase.
  • Transaksi bisnis yang melibatkan beberapa modul lebih mudah dikelola.
  • Kontrak internal dapat diubah bersama dalam satu pull request.
  • Tim onboarding lebih cepat karena alur aplikasi utama terlihat utuh.

Namun, maintainability hanya bertahan jika Anda disiplin terhadap batas modul. Jika semua modul saling membaca tabel dan memanggil helper privat, monolith akan memburuk.

Serverless lebih tahan untuk domain periferal dan event-driven

Serverless cenderung maintainable saat domainnya jelas, kecil, dan terisolasi. Misalnya:

  • Sinkronisasi webhook dari payment gateway.
  • Resize gambar setelah upload.
  • Pengiriman email atau notifikasi berbasis event.
  • Pembersihan data terjadwal.

Fungsi-fungsi ini punya kontrak sederhana, tidak butuh transaksi lintas banyak modul, dan bisa diuji sebagai unit kerja terpisah. Tetapi jika seluruh domain bisnis utama dipecah menjadi banyak function tanpa desain event yang matang, maintainability justru turun karena alur bisnis menjadi tersebar dan implisit.

Kapan monolith modular lebih tepat?

  • Tim kecil sampai menengah yang butuh iterasi cepat dengan satu workflow development.
  • Produk masih mencari bentuk dan aturan bisnis sering berubah tiap sprint.
  • API utama bersifat sinkron dan sensitif terhadap latency yang konsisten.
  • Debugging cepat lebih penting daripada elastisitas per komponen.
  • Testing end-to-end ingin sederhana dan dekat dengan perilaku nyata.
  • Ingin meminimalkan vendor lock-in dan menjaga opsi migrasi tetap terbuka.

Skenario nyata

Sebuah SaaS B2B kecil memiliki modul autentikasi, organisasi, billing, subscription, invoice, dan laporan admin. Traffic relatif stabil di jam kerja. Tim backend hanya 3 orang. Dalam situasi ini, monolith modular sering menjadi pilihan paling rasional karena:

  • Flow bisnis banyak melibatkan beberapa modul sekaligus.
  • Perubahan schema dan aturan pricing cukup sering.
  • On-call perlu sederhana; engineer yang piket harus bisa membaca satu alur sistem dengan cepat.

Kapan serverless masuk akal?

  • Workload bursty atau tidak merata sepanjang hari.
  • Fitur terisolasi dengan input-output jelas dan minim state lokal.
  • Asynchronous processing lebih dominan daripada request-response sinkron.
  • Integrasi event-driven dengan banyak webhook atau trigger file/object.
  • Tim siap dengan logging terstruktur, tracing, idempotency, dan desain retry.

Skenario nyata

Aplikasi marketplace kecil menyimpan aplikasi utama sebagai backend biasa, tetapi proses berikut dijalankan serverless:

  • Validasi dan normalisasi webhook pembayaran.
  • Pembuatan thumbnail foto produk setelah upload.
  • Pengiriman email status pesanan.
  • Rekonsiliasi harian dari file laporan pihak ketiga.

Di sini serverless masuk akal karena masing-masing pekerjaan terpisah, event-driven, dan tidak perlu menjaga server aktif sepanjang waktu.

Kapan kombinasi monolith modular + serverless layak dipilih?

Untuk banyak produk skala kecil hingga menengah, ini sering menjadi titik tengah terbaik:

  • Monolith modular menangani API utama, autentikasi, dashboard admin, dan transaksi bisnis inti.
  • Serverless menangani workload async, integrasi eksternal, file processing, scheduled tasks, dan beban sesekali yang tinggi.

Mengapa kombinasi ini sering efektif

  • Alur bisnis inti tetap mudah dipahami dan diuji.
  • Komponen yang memang cocok event-driven bisa diskalakan terpisah.
  • Cold start tidak mengganggu request utama pengguna jika fungsi dipakai untuk job async.
  • Biaya idle turun pada pekerjaan non-kritis tanpa memaksa seluruh sistem menjadi terdistribusi.

Batas yang sebaiknya dijaga

  • Jangan jadikan serverless sebagai jalan pintas untuk setiap fitur kecil.
  • Pindahkan hanya pekerjaan yang punya kontrak jelas dan observabilitas memadai.
  • Pastikan ada ownership yang jelas: mana domain inti, mana worker periferal.

Sinyal keputusan yang bisa dipakai tim

Pilih monolith modular jika banyak jawaban Anda adalah “ya”

  • Apakah domain inti masih sering berubah?
  • Apakah sebagian besar request pengguna bersifat sinkron?
  • Apakah tim ingin environment lokal yang sederhana?
  • Apakah incident response harus cepat tanpa tracing lintas layanan yang kompleks?
  • Apakah Anda ingin satu pipeline deploy yang mudah dipahami?

Pilih serverless jika banyak jawaban Anda adalah “ya”

  • Apakah beban kerja dominan berupa event atau job async?
  • Apakah traffic sangat fluktuatif atau banyak periode idle?
  • Apakah unit kerja bisa dibuat idempotent dan terisolasi?
  • Apakah tim siap mengelola observabilitas cloud-native?
  • Apakah ketergantungan pada vendor tertentu masih dapat diterima secara bisnis?

Pilih kombinasi jika banyak jawaban Anda adalah “ya”

  • Apakah aplikasi utama stabil sebagai satu service, tetapi ada beberapa proses berat di pinggir sistem?
  • Apakah Anda ingin menekan biaya untuk worker yang jarang aktif tanpa memecah domain inti?
  • Apakah alur sinkron pengguna harus tetap sederhana, tetapi proses async perlu elastis?

Checklist evaluasi sebelum migrasi

  1. Petakan workload: mana request sinkron, mana job async, mana traffic bursty.
  2. Hitung biaya total, bukan compute saja: log, tracing, queue, gateway, egress, retry, secret management.
  3. Uji latency pada jalur yang sensitif terhadap pengguna, termasuk cold start dan dependency eksternal.
  4. Pastikan strategi observabilitas: correlation ID, structured logging, metric, tracing, dashboard, alert.
  5. Definisikan model deployment: siapa yang deploy, apa rollback strategy, bagaimana canary atau gradual rollout dilakukan.
  6. Rancang testing: unit test, integration test, contract test, test data, dan simulasi event.
  7. Evaluasi koneksi database: apakah pola akses cocok untuk runtime singkat dan concurrency yang berubah-ubah.
  8. Periksa idempotency dan retry safety untuk semua proses async.
  9. Nilai vendor lock-in: apa yang sulit dipindahkan jika platform berubah.
  10. Tentukan batas domain: jangan migrasi berdasarkan tren; migrasi berdasarkan pola workload dan ownership kode.

Kesalahan umum yang membuat biaya dan kompleksitas melonjak

1. Memindahkan semuanya ke serverless sekaligus

Ini biasanya menghasilkan banyak function kecil tanpa batas domain yang sehat. Hasilnya bukan sistem yang fleksibel, tetapi sistem yang sulit dipahami.

2. Mengabaikan biaya observabilitas

Log dan tracing yang detail memang perlu, tetapi jika tidak dikelola dengan sampling, retention, dan struktur yang baik, biaya serta noise bisa naik tajam.

3. Menggunakan serverless untuk jalur sinkron yang sensitif tanpa pengujian latency

Cold start, dependency berat, dan koneksi ke layanan privat bisa menambah waktu respons secara signifikan. Uji jalur kritis sebelum migrasi penuh.

4. Monolith modular hanya di nama

Bila semua modul berbagi tabel, helper, dan query liar, masalah coupling tetap ada. Monolith modular butuh batas teknis yang ditegakkan dalam code review dan arsitektur.

5. Tidak merancang idempotency

Pada sistem async, retry adalah hal normal. Tanpa idempotency, satu event bisa menghasilkan charge ganda, email ganda, atau status data yang tidak konsisten.

6. Menganggap deployment granular selalu lebih aman

Deploy per fungsi memang mengurangi blast radius per komponen, tetapi juga bisa meningkatkan risiko ketidakcocokan kontrak event dan dependency jika governance lemah.

Tips implementasi praktis agar maintainability tetap terjaga

Untuk monolith modular

  • Buat modul berdasarkan domain, bukan hanya layer teknis.
  • Lindungi akses database per modul sebisa mungkin.
  • Gunakan queue untuk pekerjaan berat agar request utama tetap cepat.
  • Tambahkan architecture test atau linting rule untuk membatasi import lintas modul.
  • Dokumentasikan kontrak internal service yang sering dipakai lintas modul.

Untuk serverless

  • Standarkan event schema dan header seperti correlation ID.
  • Buat library internal untuk logging, tracing, dan error handling agar semua function konsisten.
  • Pastikan semua side effect penting idempotent.
  • Gunakan dead-letter handling atau mekanisme serupa untuk event gagal.
  • Bedakan function sinkron untuk user-facing API dan worker async; perlakukan keduanya dengan target latency yang berbeda.

Kesimpulan

Dalam konteks monolith modular vs serverless untuk produk web/backend skala kecil hingga menengah, pilihan terbaik biasanya ditentukan oleh biaya perubahan, bukan sekadar biaya compute. Jika domain inti masih aktif berubah, tim kecil, dan kebutuhan utama adalah development serta debugging yang cepat, monolith modular hampir selalu menjadi default yang kuat. Jika workload Anda jelas event-driven, bursty, dan terisolasi, serverless bisa sangat efektif. Jika keduanya hadir bersamaan, gunakan kombinasi: pertahankan inti bisnis dalam monolith modular, dan pindahkan beban async atau periferal ke serverless.

Pertanyaan terbaik bukan “arsitektur mana yang paling canggih?”, tetapi “arsitektur mana yang membuat tim saya bisa mengirim perubahan dengan aman, memahami sistem saat rusak, dan membayar kompleksitas yang memang sepadan dengan nilai bisnisnya?”