Dalam perbandingan modular monolith vs microservices, modular monolith biasanya menjadi pilihan awal yang lebih aman untuk aplikasi yang sedang tumbuh. Microservices layak dipertimbangkan ketika ada batas domain yang stabil, kebutuhan scaling atau isolasi kegagalan yang berbeda, dan tim independen yang benar-benar mampu memiliki layanan dari pengembangan hingga operasi.

Jangan memisahkan layanan hanya karena jumlah fitur atau baris kode bertambah. Pemisahan menghasilkan manfaat jika batas layanan mengurangi koordinasi dan memungkinkan deployment, scaling, atau pemulihan secara independen. Jika batas domain masih sering berubah, microservices justru memindahkan kompleksitas dari pemanggilan fungsi dan transaksi lokal menjadi komunikasi jaringan, kontrak API, observability, serta konsistensi data terdistribusi.

Perbedaan inti: batas modul atau batas proses

Modular monolith menjalankan beberapa modul domain di dalam satu unit deployment. Setiap modul tetap mempunyai API internal, aturan bisnis, dan kepemilikan data yang jelas, tetapi komunikasi antarmodul dapat berlangsung di dalam proses yang sama.

Microservices menempatkan kemampuan bisnis dalam proses atau layanan terpisah. Setiap layanan idealnya dapat dikembangkan, dirilis, diskalakan, dan dipulihkan tanpa mengharuskan deployment serentak dengan layanan lain.

Perbedaan utama bukan jumlah repository atau container. Pertanyaan yang lebih penting adalah apakah komponen dapat berubah, dirilis, dan beroperasi secara independen.

Batas domain harus mendahului batas jaringan

Batas layanan sebaiknya mengikuti bounded context atau kemampuan bisnis, misalnya katalog, pemesanan, pembayaran, dan pengiriman. Membuat satu layanan per tabel biasanya menghasilkan layanan yang terlalu kecil dan saling memanggil untuk menyelesaikan satu operasi sederhana.

Sebuah batas domain cukup matang ketika:

  • Istilah, aturan, dan tanggung jawab bisnisnya dapat dijelaskan secara mandiri.
  • Data yang menjadi sumber kebenaran dapat dimiliki oleh satu modul atau layanan.
  • Perubahan di dalam batas tersebut jarang membutuhkan perubahan sinkron pada banyak komponen lain.
  • Input, output, kegagalan, dan kebutuhan konsistensinya dapat dinyatakan sebagai kontrak.

Jika tim masih sering memindahkan aturan bisnis antararea, pertahankan batas tersebut sebagai modul dalam monolith. Refactoring batas modul jauh lebih murah daripada memindahkan API, data, antrean, dan ownership di antara layanan produksi.

Modular monolith vs microservices berdasarkan faktor teknis

FaktorModular monolithMicroservices
CouplingDapat rendah jika dependency antarmodul dikendalikan, tetapi akses internal yang tidak disiplin mudah menimbulkan coupling tersembunyi.Kontrak jaringan terlihat lebih jelas, tetapi coupling tetap dapat terjadi melalui API sinkron, event, skema bersama, atau deployment yang harus serentak.
ScalingSeluruh aplikasi biasanya diskalakan sebagai satu unit, termasuk bagian yang bebannya rendah.Layanan dengan pola beban khusus dapat diskalakan secara independen.
TransaksiTransaksi ACID lintas modul relatif sederhana jika menggunakan database yang sama.Transaksi lintas layanan memerlukan saga, outbox, idempotensi, atau kompensasi; konsistensi sering bersifat eventual.
LatensiPemanggilan lokal cepat dan tidak terkena kegagalan jaringan.Setiap hop menambah latensi, serialization, timeout, retry, dan kemungkinan kegagalan parsial.
DeploymentPipeline lebih sederhana, tetapi satu perubahan dapat mengharuskan deployment seluruh aplikasi.Deployment independen dimungkinkan, tetapi membutuhkan kompatibilitas kontrak dan pengelolaan banyak artefak.
ObservabilityLog dan stack trace lebih mudah diikuti dalam satu proses.Memerlukan korelasi log, metrik per layanan, distributed tracing, dan pelacakan pesan asinkron.
TestingIntegration test dapat berjalan dengan lebih sedikit dependency eksternal.Memerlukan kombinasi unit test, contract test, integration test, dan sejumlah kecil end-to-end test.
TimCocok untuk satu atau beberapa tim yang masih banyak berkoordinasi.Efektif ketika layanan dimiliki tim otonom yang juga bertanggung jawab atas operasi produksi.
BiayaLebih sedikit runtime, pipeline, dashboard, dan komponen jaringan.Ada biaya minimum per layanan untuk compute, observability, deployment, keamanan, dan on-call.

Coupling tidak otomatis hilang karena HTTP

Mengganti pemanggilan fungsi dengan HTTP tidak menghapus coupling. Jika layanan checkout tidak dapat dirilis tanpa versi tertentu dari layanan katalog, keduanya masih mempunyai temporal atau deployment coupling. Jika satu request pengguna harus melewati enam layanan sinkron, ketersediaan keseluruhan juga bergantung pada seluruh rantai tersebut.

Kurangi coupling dengan kontrak yang sempit, perubahan backward-compatible, operasi idempoten, dan event yang mewakili fakta bisnis. Hindari API yang mengekspos struktur tabel internal atau mengharuskan klien memahami alur kerja privat layanan.

Scaling independen harus didukung pola beban nyata

Microservices bernilai ketika bagian tertentu memiliki pola beban yang berbeda secara konsisten. Contohnya, pemrosesan gambar bersifat CPU-intensive, pencarian mempunyai kebutuhan read throughput tinggi, sedangkan checkout membutuhkan konsistensi transaksi dan latency yang terkontrol.

Sebelum ekstraksi, pastikan bottleneck memang berada pada batas yang akan dipisahkan. Query lambat, indeks yang hilang, alokasi koneksi database yang buruk, atau pekerjaan berat yang seharusnya masuk queue tidak otomatis memerlukan microservices. Banyak masalah scaling dapat diselesaikan lebih murah melalui profiling, caching, asynchronous job, read replica, atau pemisahan worker dari web process.

Konsistensi transaksi dan kepemilikan data

Dalam modular monolith, satu transaksi dapat memperbarui pesanan, stok, dan catatan audit secara atomik. Setelah domain dipisahkan, transaksi database tidak lagi mencakup seluruh layanan. Operasi harus dirancang untuk menghadapi pesan terlambat, duplikat, atau gagal diproses.

Prinsip pentingnya adalah satu sumber kebenaran dan satu penulis untuk setiap data bisnis. Layanan lain sebaiknya mengakses data melalui API, event, atau proyeksi lokal, bukan menulis langsung ke tabel milik layanan tersebut.

Pola transactional outbox dapat menyimpan perubahan domain dan event dalam satu transaksi lokal:

BEGIN;

UPDATE orders
SET status = 'PAID'
WHERE id = :order_id AND status = 'AWAITING_PAYMENT';

INSERT INTO outbox_events (event_id, event_type, aggregate_id, payload)
VALUES (:event_id, 'OrderPaid', :order_id, :payload);

COMMIT;

Worker kemudian menerbitkan record outbox ke broker dan menandainya telah diproses. Konsumen tetap harus idempoten karena penerbitan atau pengiriman dapat terjadi lebih dari sekali. Pola ini menghindari dual write langsung ke database dan broker, tetapi menambah kebutuhan monitoring backlog, retry, serta penanganan pesan gagal.

Deployment, observability, dan testing

Pada microservices, setiap layanan membutuhkan pipeline, konfigurasi runtime, pengelolaan secret, health check, kebijakan timeout, rollback, dashboard, alert, dan prosedur insiden. Container atau orchestrator membantu otomasi, tetapi tidak menghilangkan pekerjaan operasional tersebut.

Minimal, request lintas layanan perlu membawa correlation ID. Log harus terstruktur dan menyertakan nama layanan, versi deployment, operasi, serta hasilnya. Distributed tracing berguna untuk menemukan hop lambat, tetapi tidak menggantikan metrik bisnis seperti jumlah pesanan gagal atau event yang tertunda.

Strategi testing juga berubah:

  • Unit test menguji aturan bisnis tanpa jaringan atau database jika memungkinkan.
  • Integration test menguji adapter database, broker, cache, dan API nyata.
  • Contract test memverifikasi provider dan consumer masih memahami skema yang sama.
  • End-to-end test hanya digunakan untuk alur kritis karena lebih lambat dan rentan terhadap kegagalan lingkungan.

Kesalahan umum adalah mengandalkan staging end-to-end untuk menemukan seluruh incompatibility. Kontrak sebaiknya divalidasi lebih awal dalam pipeline masing-masing layanan.

Matriks keputusan berdasarkan tim dan pola beban

Ukuran tim bukan aturan mutlak, tetapi dapat menunjukkan apakah organisasi mampu menanggung ownership dan operasi terdistribusi. Gunakan matriks berikut sebagai titik awal, lalu validasi dengan data perubahan dan beban produksi.

KondisiPola bebanPilihan awalAlasan
1–5 engineer, satu backlog utamaRelatif seragam atau masih berubah cepatModular monolithBiaya koordinasi dan operasi microservices kemungkinan lebih besar daripada manfaatnya.
6–15 engineer, beberapa area domainAda hotspot, tetapi masih dapat ditangani dengan worker atau optimasi databaseModular monolith dengan boundary ketatBangun ownership modul dan ukur coupling sebelum menambah jaringan.
6–15 engineer, satu kemampuan mempunyai lonjakan atau kebutuhan isolasi yang jelasBeban sangat berbeda dan dapat dipisahkanEkstrak satu layanan terpilihMemberikan scaling atau failure isolation tanpa memecah seluruh sistem.
Beberapa squad otonomDomain stabil dan ritme rilis berbedaMicroservices selektifTim dapat memiliki layanan, data, deployment, dan on-call secara end-to-end.
Banyak tim, tetapi setiap fitur menyentuh seluruh domainBeban mungkin tinggi, batas bisnis belum jelasPerbaiki batas modular terlebih dahuluMemecah proses sebelum memecah ownership akan menciptakan distributed monolith.
Tim platform dan operasional sudah matangHotspot independen, kebutuhan availability atau compliance berbedaMicroservices pada bounded context yang sesuaiPlatform dapat menstandarkan deployment, observability, keamanan, dan respons insiden.

Jangan menggunakan angka engineer sebagai satu-satunya pemicu. Tiga tim yang selalu mengubah kode yang sama belum siap memperoleh manfaat layanan independen. Sebaliknya, satu tim kecil kadang perlu memisahkan worker komputasi berat karena alasan isolasi sumber daya, meskipun belum membutuhkan arsitektur microservices secara luas.

Contoh estimasi biaya operasional

Berikut ilustrasi, bukan harga universal. Asumsikan biaya bulanan internal atau penyedia infrastruktur sebagai berikut: instance aplikasi kecil 40 unit biaya, instance lebih besar 80, database terkelola 150, load balancer 30–50, broker 100, serta observability 50–150. Angka harus diganti dengan harga penyedia, region, kebutuhan availability, retensi log, dan trafik aktual.

KomponenModular monolithEnam microservices
Compute aplikasi2 × 80 = 1606 layanan × 2 instance × 40 = 480
Database150150–lebih tinggi, tergantung isolasi data
Load balancer atau gateway3050
Broker pesanOpsional100 jika diperlukan
Observability50150 karena volume dan kardinalitas meningkat
Perkiraan minimumSekitar 390Sekitar 930 sebelum database tambahan

Ilustrasi tersebut belum memasukkan trafik jaringan, penyimpanan log, lingkungan nonproduksi, backup, pipeline CI, sertifikat, secret management, maupun tenaga on-call. Dalam banyak organisasi, biaya engineer untuk membangun dan merawat platform lebih besar daripada tagihan compute. Karena itu, estimasi keputusan harus mencakup total cost of ownership, bukan hanya jumlah container.

Indikator bahwa monolith mulai menghambat

Monolith layak dipecah ketika hambatan muncul berulang kali, dapat diukur, dan memiliki hubungan jelas dengan suatu batas domain. Indikator yang relevan antara lain:

  • Tim yang berbeda terus mengalami konflik perubahan pada modul yang sebenarnya memiliki tanggung jawab terpisah.
  • Deployment satu area berisiko atau tertunda karena harus menguji ulang seluruh aplikasi.
  • Satu workload menghabiskan CPU, memori, connection pool, atau kapasitas database sehingga mengganggu area lain.
  • Kebutuhan availability, keamanan, compliance, atau jadwal pemeliharaan suatu domain berbeda secara substansial.
  • Waktu lead perubahan meningkat karena ownership tidak jelas dan setiap perubahan membutuhkan koordinasi lintas tim.
  • Kegagalan pada proses nonkritis berulang kali menjatuhkan alur bisnis kritis.
  • Build dan test suite menjadi lambat karena boundary tidak dapat diuji secara terisolasi, bukan sekadar karena pipeline belum dioptimalkan.
  • Satu modul mempunyai pola rilis dan scaling yang stabil serta berbeda dari bagian lain.

Kumpulkan bukti dari deployment frequency, lead time, change failure rate, waktu pemulihan, profil resource, dependency graph, dan catatan insiden. Jangan menetapkan ambang universal; bandingkan tren sistem dengan target layanan dan biaya perubahan organisasi Anda.

Risiko memecah layanan terlalu dini

  • Distributed monolith: layanan terpisah tetapi harus dirilis bersama dan tidak dapat berjalan ketika layanan lain tidak tersedia.
  • Batas domain salah: perubahan bisnis sederhana membutuhkan modifikasi banyak API dan event.
  • Chatty communication: satu request menghasilkan banyak panggilan kecil dan memperbesar latensi serta risiko timeout.
  • Data terfragmentasi tanpa strategi: reporting, rekonsiliasi, dan koreksi data menjadi sulit karena ownership tidak jelas.
  • Konsistensi tidak dipahami: tim menganggap message delivery selalu tepat satu kali atau retry selalu aman.
  • Beban operasional berlebihan: tim produk menghabiskan waktu mengelola pipeline, dashboard, dan insiden alih-alih fitur.
  • Keamanan makin luas: bertambahnya endpoint, identity antarlayanan, secret, dan dependency memperbesar attack surface.

Microservices juga tidak membenarkan penggunaan teknologi berbeda untuk setiap layanan. Standardisasi runtime, logging, autentikasi, deployment, dan library operasional biasanya lebih bernilai daripada kebebasan teknologi yang tidak mempunyai kebutuhan nyata.

Membangun modular monolith yang siap diekstrak

Modular monolith bukan monolith tanpa struktur. Tujuannya adalah mempertahankan kesederhanaan satu deployment sambil membuat dependency dan ownership eksplisit.

Struktur generik berikut memisahkan domain, use case, adapter infrastruktur, dan API publik setiap modul:

src/
  modules/
    orders/
      domain/
      application/
      infrastructure/
      public-api/
    payments/
      domain/
      application/
      infrastructure/
      public-api/
  bootstrap/
  shared-kernel/

Modul orders hanya boleh mengakses kemampuan pembayaran melalui payments/public-api, bukan repository atau tabel internal pembayaran. Aturan ini dapat diperiksa melalui architecture test, dependency linting, atau aturan build yang sesuai dengan bahasa yang digunakan.

Praktik yang membantu ekstraksi di kemudian hari:

  1. Tetapkan owner dan dokumentasikan tanggung jawab tiap modul.
  2. Berikan skema atau namespace data yang jelas, meskipun masih memakai satu database fisik.
  3. Larangan akses langsung ke tabel milik modul lain harus ditegakkan dalam code review dan pengujian.
  4. Gunakan port atau interface untuk interaksi antarmodul.
  5. Catat event domain penting tanpa langsung menganggap semua komunikasi harus asinkron.
  6. Ukur latensi, error, dan konsumsi resource per modul atau operasi.
  7. Hindari shared-kernel yang menjadi tempat semua model dan utilitas bisnis.

Database bersama masih dapat digunakan selama fase modularisasi, tetapi kepemilikan tabel harus jelas. Jika semua modul bebas membaca dan menulis semua tabel, pemisahan proses nantinya akan sulit meskipun struktur source code terlihat rapi.

Migrasi bertahap dengan strangler pattern

Strangler pattern memindahkan kemampuan secara bertahap dari monolith ke layanan baru. Router, API gateway, atau lapisan aplikasi mengarahkan operasi yang sudah dipindahkan ke layanan baru, sementara operasi lain tetap ditangani monolith. Pendekatan ini mengurangi risiko dibandingkan penulisan ulang sekaligus.

1. Pilih seam yang sempit dan bernilai

Pilih modul dengan batas domain jelas, sedikit transaksi lintas batas, dan alasan ekstraksi yang dapat diukur. Kandidat pertama sebaiknya bukan alur paling kritis jika tim belum mempunyai pengalaman operasional layanan terdistribusi.

2. Definisikan kontrak sebelum memindahkan implementasi

Kontrak harus menjelaskan request, response, error, autentikasi, timeout, idempotensi, dan aturan kompatibilitas. Contoh kontrak ringkas untuk operasi internal:

openapi: 3.0.3
paths:
  /internal/v1/orders/{orderId}/reservations:
    post:
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          schema:
            type: string
      responses:
        '202':
          description: Reservation request accepted
        '409':
          description: Reservation conflicts with current state

Status 202 hanya tepat jika pekerjaan memang diproses secara asinkron. Jika hasil harus tersedia sebelum request selesai, gunakan semantik respons sinkron yang sesuai. Yang penting, kontrak mencerminkan perilaku aktual dan diuji oleh provider serta consumer.

3. Alihkan dependency internal melalui adapter

Sebelum layanan dipisahkan, ubah pemanggilan langsung menjadi interface lokal. Implementasi pertama tetap memanggil modul dalam proses. Setelah layanan baru siap, ganti adapter tersebut dengan klien HTTP atau messaging. Cara ini membatasi perubahan pada caller dan menyediakan titik untuk timeout, retry, circuit breaking, serta telemetry.

Retry hanya boleh diterapkan pada kegagalan sementara dan operasi yang idempoten. Beri batas waktu eksplisit dan hindari retry berlapis pada gateway, client library, dan worker karena dapat melipatgandakan beban saat dependency sedang bermasalah.

4. Migrasikan data dengan satu penulis

Tentukan sistem mana yang menjadi sumber kebenaran pada setiap fase. Hindari dual write tanpa koordinasi karena satu penulisan dapat berhasil sementara yang lain gagal. Pilihan yang umum adalah:

  • Backfill data historis, lalu alirkan perubahan baru melalui outbox atau change data capture.
  • Pertahankan monolith sebagai penulis sementara dan jadikan layanan baru sebagai pembaca.
  • Alihkan kepemilikan penulisan pada cutover yang terkontrol, lalu sediakan proyeksi bagi consumer lama.

Rekonsiliasi jumlah record, versi, dan state bisnis sebelum serta sesudah cutover. Jangan hanya membandingkan keberhasilan request; keterlambatan event dan data yatim juga perlu diperiksa.

5. Rilis secara terkendali

Gunakan feature flag atau routing berdasarkan tenant, region, atau persentase trafik jika model bisnis memungkinkan. Shadow traffic dapat membantu membandingkan hasil baca, tetapi jangan menduplikasi operasi yang mempunyai efek samping. Siapkan rollback yang mengembalikan routing tanpa menghasilkan dua penulis aktif.

6. Hapus implementasi lama setelah stabil

Strangler pattern gagal jika jalur lama dibiarkan selamanya. Setelah layanan baru memenuhi target error, latency, data consistency, dan operasional, hentikan penulisan lama, hapus adapter yang tidak diperlukan, arsipkan tabel, dan perbarui runbook serta diagram ownership.

Checklist sebelum mengekstrak modul

  • Apakah batas domain dapat dijelaskan tanpa merujuk pada struktur tabel?
  • Apakah ekstraksi menyelesaikan masalah terukur pada scaling, deployment, ownership, keamanan, atau failure isolation?
  • Apakah modul sudah mempunyai API internal dan dependency yang terkendali?
  • Siapa pemilik layanan, data, pipeline, dashboard, alert, dan on-call?
  • Apakah layanan dapat dirilis tanpa deployment serentak dengan consumer?
  • Bagaimana kontrak diuji dan bagaimana perubahan backward-compatible dilakukan?
  • Operasi mana yang membutuhkan konsistensi kuat dan mana yang menerima eventual consistency?
  • Bagaimana timeout, retry, idempotensi, kompensasi, dan pesan duplikat ditangani?
  • Di mana sumber kebenaran data berada selama migrasi dan setelah cutover?
  • Bagaimana data historis dipindahkan dan direkonsiliasi?
  • Apakah observability dapat mengikuti satu request atau event lintas layanan?
  • Berapa biaya compute, database, broker, log, CI/CD, lingkungan nonproduksi, dan tenaga operasi?
  • Apakah ada rencana canary, rollback, dan penghapusan implementasi lama?
  • Bisakah masalah yang sama diselesaikan lebih murah dengan modularisasi, queue, caching, atau optimasi database?

Kesimpulan

Pilih modular monolith ketika batas domain masih berkembang, tim belum membutuhkan deployment independen, transaksi lintas modul dominan, dan biaya operasional perlu dijaga rendah. Monolith yang modular dapat mendukung pertumbuhan cukup lama selama dependency, ownership, serta akses data dikendalikan.

Pilih microservices secara selektif ketika suatu domain mempunyai ownership yang matang, pola scaling atau availability berbeda, dan manfaat independensinya melebihi biaya jaringan serta operasi terdistribusi. Mulailah dari satu ekstraksi yang dapat diukur, gunakan strangler pattern, pertahankan kontrak backward-compatible, dan pindahkan kepemilikan data secara eksplisit. Tujuannya bukan menghasilkan sebanyak mungkin layanan, melainkan membentuk batas sistem yang mempercepat perubahan tanpa memindahkan kompleksitas ke tempat yang lebih sulit didiagnosis.