Memilih monolith, modular monolith, atau microservices bukan soal mengikuti tren, tetapi soal menyesuaikan arsitektur dengan ukuran tim, kompleksitas domain, pola perubahan kode, dan kemampuan operasional. Untuk banyak aplikasi web/backend, monolith atau modular monolith sering menjadi pilihan paling rasional di tahap awal karena lebih sederhana untuk dikembangkan, diuji, dan dioperasikan.

Microservices baru benar-benar bernilai ketika ada alasan kuat: domain yang cukup terpisah, kebutuhan scaling yang berbeda antar komponen, tim yang bisa bekerja lebih mandiri, atau batas operasional yang memang sudah terlalu berat ditangani dalam satu aplikasi. Jika dipilih terlalu cepat, microservices sering memindahkan kompleksitas dari kode ke jaringan, deployment, observability, dan koordinasi antar tim.

Apa perbedaan monolith, modular monolith, dan microservices?

Monolith

Monolith adalah aplikasi yang dibangun dan dideploy sebagai satu unit. Biasanya satu repository, satu pipeline utama, satu artefak deploy, dan sering kali satu database utama. Semua fitur berjalan dalam satu proses aplikasi, meskipun secara internal tetap bisa dipisah dengan package atau folder.

  • Kelebihan: sederhana, cepat dikembangkan, debugging lebih mudah, transaksi lintas modul lebih mudah, local development ringan.
  • Kekurangan: jika struktur internal buruk, coupling meningkat, build/test makin lambat, dan perubahan kecil bisa memengaruhi banyak area.

Modular Monolith

Modular monolith tetap dideploy sebagai satu aplikasi, tetapi struktur internalnya dipisahkan secara tegas ke dalam modul dengan batas yang jelas. Setiap modul memiliki API internal, aturan dependensi, dan tanggung jawab domain yang terdefinisi.

  • Kelebihan: menjaga kesederhanaan operasional monolith sambil mengurangi coupling, lebih mudah diuji per modul, menjadi landasan evolusi jika suatu saat perlu dipisah.
  • Kekurangan: perlu disiplin desain, enforcement dependensi, dan review arsitektur. Tanpa itu, modular monolith bisa berubah menjadi monolith biasa yang hanya berganti nama.

Microservices

Microservices memecah sistem menjadi beberapa service independen yang berkomunikasi melalui network, biasanya HTTP/gRPC dan/atau messaging. Setiap service punya boundary sendiri, deployment sendiri, dan sering kali penyimpanan data sendiri.

  • Kelebihan: isolasi domain lebih kuat, deployment independen, scaling per service, kebebasan implementasi lebih besar.
  • Kekurangan: distribusi sistem menambah kompleksitas besar: network failure, observability terdistribusi, versioning kontrak API, konsistensi data, keamanan antarlayanan, dan overhead operasional.

Tabel perbandingan praktis

AspekMonolithModular MonolithMicroservices
Unit deploymentSatuSatuBanyak
Kompleksitas operasionalRendahRendah-menengahTinggi
Kompleksitas kode internalBisa tinggi jika tidak terstrukturLebih terkontrolTersebar di banyak service
ObservabilityLebih mudahMasih relatif mudahJauh lebih sulit
ScalingSkala satu aplikasi penuhSkala satu aplikasi penuhBisa per service
Batas domainSering kaburLebih tegasSangat penting dan wajib jelas
Kecepatan tim kecilSangat baikSangat baikSering lambat karena overhead
Konsistensi dataPaling mudahMudahLebih sulit, sering eventual consistency
Testing end-to-endLebih sederhanaMasih sederhanaLebih mahal dan rapuh
Kemandirian timTerbatasMenengahTinggi jika boundary benar
Biaya infrastrukturRendahRendah-menengahTinggi

Trade-off teknis yang paling menentukan

1. Biaya operasional

Monolith paling murah secara operasional. Anda cukup mengelola satu deployment utama, satu set log, satu health check, dan biasanya satu jalur incident response. Modular monolith masih dekat dengan model ini.

Microservices menaikkan biaya operasional secara signifikan. Anda membutuhkan service discovery atau routing yang rapi, pengelolaan secret untuk banyak service, monitoring per service, distributed tracing, alerting yang lebih presisi, dan pipeline CI/CD yang lebih banyak. Bahkan jika setiap service tampak kecil, total sistemnya bisa jauh lebih sulit dioperasikan.

2. Kompleksitas deployment

Pada monolith, satu commit dapat menghasilkan satu artefak deploy. Rollback juga lebih sederhana. Risiko utamanya adalah blast radius: satu deploy bisa menyentuh seluruh aplikasi.

Pada microservices, deploy per service memang lebih independen, tetapi dependency antar service membuat masalah baru: kompatibilitas API, urutan rollout, migrasi schema, sinkronisasi event, dan kebutuhan backward compatibility. Kemandirian deployment hanya benar-benar terasa jika boundary service stabil dan kontrak antarlayanan dikelola dengan baik.

3. Observability dan debugging

Masalah yang terjadi di monolith biasanya lebih mudah ditelusuri karena stack trace, log, dan konteks request berada dalam satu proses. Pada microservices, satu request pengguna bisa melewati banyak service, queue, retry, dan background worker. Tanpa correlation ID, centralized logging, metrics, dan tracing, debugging akan sangat lambat.

Jika tim Anda belum punya disiplin log terstruktur, metrics, tracing, dan incident review yang baik, microservices biasanya memperburuk masalah yang sebelumnya belum terselesaikan.

4. Batas domain

Microservices membutuhkan domain boundary yang cukup matang. Jika batas tanggung jawab belum jelas, pemecahan service justru menciptakan coupling lintas jaringan. Hasilnya bukan loose coupling, tetapi distributed monolith: banyak service yang harus selalu berubah bersama.

Modular monolith cocok ketika domain sudah mulai kompleks tetapi boundary masih berkembang. Anda bisa memaksa separation secara internal lebih dulu, tanpa memikul biaya distribusi sistem.

5. Pola scaling

Salah satu alasan valid memakai microservices adalah ketika pola beban sangat berbeda. Misalnya, modul pencarian, pemrosesan video, atau notifikasi memiliki kebutuhan CPU, memori, throughput, atau latency yang tidak sama dengan modul CRUD utama.

Namun, jangan salah kaprah: banyak kasus scaling dapat diselesaikan di monolith dengan caching, queue, read replica, optimasi query, atau memindahkan pekerjaan berat ke worker terpisah tanpa memecah aplikasi menjadi banyak service.

6. Maintainability jangka panjang

Maintainability tidak hanya ditentukan oleh ukuran codebase, tetapi juga oleh jumlah moving parts. Monolith yang terstruktur baik bisa lebih mudah dipelihara daripada microservices yang boundaries-nya buruk. Sebaliknya, monolith yang semua modulnya saling mengakses tabel dan utilitas secara bebas akan sulit berkembang.

Pertanyaan yang lebih penting bukan “seberapa besar aplikasi ini”, melainkan “seberapa jelas batas tanggung jawabnya, seberapa mudah perubahan dilakukan dengan aman, dan seberapa mahal memahami dampak suatu perubahan”.

Kapan monolith cocok dipilih?

Pilih monolith ketika:

  • Tim masih kecil, misalnya 1-5 engineer.
  • Produk masih mencari bentuk dan kebutuhan sering berubah.
  • Domain belum stabil sehingga boundary service belum jelas.
  • Kecepatan delivery lebih penting daripada pemisahan operasional.
  • Sistem masih bisa ditangani oleh satu deployment dan satu database utama.
  • Anda ingin mengurangi biaya DevOps, observability, dan koordinasi lintas service.

Monolith bukan berarti kode harus acak. Bahkan pada monolith, pemisahan domain, layer yang jelas, dan kontrak internal tetap penting agar evolusi berikutnya tidak mahal.

Anti-pattern umum pada monolith

  • Big ball of mud: semua modul bebas saling import dan berbagi tabel.
  • Shared utility berlebihan: helper global menjadi tempat logika bisnis tersembunyi.
  • Lapisan terlalu generik: abstraksi berlebihan yang justru memperumit perubahan sederhana.
  • Satu database, semua bebas query: boundary domain runtuh karena semua kode mengakses semua tabel.

Kapan modular monolith cocok dipilih?

Modular monolith cocok saat aplikasi mulai kompleks, tim bertambah, dan Anda membutuhkan boundary domain yang lebih tegas tanpa menanggung beban penuh microservices. Ini sering menjadi titik tengah terbaik untuk aplikasi web/backend yang sedang bertumbuh.

Gunakan modular monolith ketika:

  • Anda ingin memisahkan domain seperti user, billing, order, inventory, dan notification secara tegas.
  • Tim mulai bekerja paralel dan membutuhkan area ownership yang lebih jelas.
  • Monolith sudah terasa padat, tetapi masalah utamanya adalah struktur internal, bukan keterbatasan deployment.
  • Anda ingin menyiapkan jalan keluar ke microservices hanya jika nanti benar-benar diperlukan.

Contoh struktur modular monolith

src/
  modules/
    user/
      application/
      domain/
      infrastructure/
      api/
    billing/
      application/
      domain/
      infrastructure/
      api/
    order/
      application/
      domain/
      infrastructure/
      api/
  shared/
    logging/
    auth/
    db/

Prinsip pentingnya:

  • Modul berkomunikasi melalui interface atau application service, bukan langsung ke detail internal modul lain.
  • Query langsung ke tabel milik modul lain dibatasi atau dilarang.
  • Event internal dapat dipakai untuk mengurangi coupling.
  • Test bisa dijalankan per modul, bukan selalu seluruh aplikasi.

Anti-pattern umum pada modular monolith

  • Modul hanya foldering: struktur terlihat rapi tetapi dependensi tetap liar.
  • Shared kernel terlalu gemuk: terlalu banyak kode “bersama” sampai boundary modul kehilangan arti.
  • Cross-module DB access: modul A langsung membaca/menulis tabel milik modul B.
  • Kontrak internal tidak dijaga: perubahan internal modul memecahkan modul lain.

Kapan microservices cocok dipilih?

Microservices cocok jika kebutuhan bisnis dan organisasi sudah memang menuntut pemisahan independen. Bukan karena aplikasi “besar”, tetapi karena coupling, kebutuhan scaling, dan ritme perubahan antar domain sudah cukup berbeda.

Pilih microservices ketika beberapa kondisi berikut nyata terjadi:

  • Domain tertentu dapat berdiri cukup mandiri dengan kontrak yang jelas.
  • Tim sudah cukup matang untuk mengelola CI/CD, observability, alerting, secret, dan incident response lintas service.
  • Deployment independen memberi nilai nyata, misalnya billing harus dirilis lebih sering tanpa menunggu modul lain.
  • Pola beban antar domain berbeda jauh dan scaling satu aplikasi penuh terlalu boros.
  • Kegagalan di satu area perlu diisolasi agar tidak menjatuhkan seluruh sistem.
  • Organisasi sudah memiliki ownership service yang jelas, bukan sekadar banyak repo tanpa tanggung jawab.

Masalah nyata yang harus siap ditangani

  • Network failure: timeout, retry storm, partial failure, circuit breaker, idempotency.
  • Data consistency: transaksi ACID lintas service sulit; sering perlu eventual consistency.
  • API contract: versioning, backward compatibility, schema evolution.
  • Operational overhead: log aggregation, tracing, dashboard, on-call, SLO/SLI.
  • Security: service-to-service auth, secret rotation, akses antar lingkungan.

Anti-pattern umum pada microservices

  • Distributed monolith: service banyak tetapi harus selalu deploy bersama.
  • Shared database antar service: deployment independen hilang karena schema menjadi coupling utama.
  • Service terlalu kecil: pemecahan berdasarkan CRUD teknis, bukan domain bisnis.
  • Sinkron semua lewat HTTP: rantai dependency terlalu panjang, latency dan failure meningkat.
  • Belum siap observability: insiden sulit didiagnosis karena tidak ada tracing dan correlation ID.

Skenario praktis: tim kecil vs tim bertumbuh

Skenario 1: tim kecil, produk masih berubah cepat

Misalnya tim terdiri dari 3 engineer yang membangun SaaS internal dengan fitur autentikasi, organisasi, billing sederhana, dashboard, dan notifikasi email. Pada kondisi ini, monolith atau modular monolith hampir selalu lebih masuk akal daripada microservices.

Alasannya sederhana:

  • Kecepatan perubahan fitur lebih penting daripada isolasi operasional.
  • Tim yang sama mengerjakan hampir semua area.
  • Biaya setup banyak service, pipeline, monitoring, dan deployment belum sebanding dengan manfaatnya.
  • Boundary domain mungkin belum stabil.

Rekomendasi: mulai dari monolith, tetapi susun modul berdasarkan domain sejak awal. Jika billing, order, dan notification sudah punya boundary jelas, bentuklah modular monolith sebelum berpikir ke microservices.

Skenario 2: tim bertumbuh, domain mulai jelas

Misalnya produk berkembang menjadi platform e-commerce. Tim bertambah menjadi 15-25 engineer. Modul order, catalog, inventory, payment, shipping, dan recommendation memiliki ritme perubahan berbeda. Traffic juga tidak merata: search dan recommendation jauh lebih berat dibanding admin backend.

Di sini modular monolith sering menjadi langkah antara yang sangat sehat. Beberapa workload berat seperti indexing, recommendation, atau asynchronous fulfillment dapat dipisah sebagai service atau worker terpisah lebih dulu, tanpa harus memecah seluruh sistem.

Jika kemudian inventory dan payment benar-benar membutuhkan deploy independen, ownership tim jelas, dan kontrak domain matang, barulah service tertentu dipisahkan satu per satu.

Contoh evolusi bertahap yang aman

Pendekatan berikut membantu menghindari overengineering:

  1. Mulai dari monolith yang rapi. Pisahkan domain di level package/module, bukan berdasarkan layer teknis semata.
  2. Terapkan batas dependensi. Modul berinteraksi lewat interface atau event internal.
  3. Pisahkan workload berat dulu. Gunakan queue/worker untuk email, report, indexing, image processing, atau webhook.
  4. Ukur bottleneck nyata. Pastikan masalahnya memang coupling atau scaling, bukan query lambat atau cache yang belum ada.
  5. Ekstrak satu service yang jelas. Pilih domain dengan boundary kuat, dependency sedikit, dan nilai operasional yang nyata.
  6. Bangun capability platform seperlunya. Logging, tracing, metrics, service auth, dan deployment automation harus ikut matang.

Contoh boundary internal sebelum ekstraksi service

// Order module tidak mengakses tabel billing secara langsung.
// Ia memanggil kontrak aplikasi yang disediakan modul billing.

interface BillingService {
  createInvoice(orderId: string, amount: number): Promise<void>;
}

class PlaceOrderHandler {
  constructor(
    private readonly billingService: BillingService,
    private readonly orderRepository: OrderRepository
  ) {}

  async execute(cmd: { customerId: string; amount: number }) {
    const order = await this.orderRepository.create(cmd);
    await this.billingService.createInvoice(order.id, cmd.amount);
    return order;
  }
}

Jika suatu hari modul billing dipisahkan menjadi service, kontrak di atas dapat diganti implementasinya menjadi pemanggilan HTTP/gRPC atau event publisher. Yang berubah adalah adapter-nya, bukan alur bisnis inti.

Checklist keputusan: pilih yang mana?

Gunakan checklist ini secara jujur. Jika banyak jawaban masih “belum” atau “tidak yakin”, biasanya microservices terlalu dini.

Pilih monolith jika:

  • Tim kecil dan tanggung jawab masih tumpang tindih.
  • Produk masih sering pivot atau kebutuhan belum stabil.
  • Satu deployment masih cukup cepat dan aman.
  • Masalah utama Anda adalah delivery fitur, bukan isolasi operasional.
  • Insiden lebih sering berasal dari bug aplikasi, bukan bottleneck arsitektur.

Pilih modular monolith jika:

  • Codebase mulai besar tetapi masih logis dideploy sebagai satu unit.
  • Anda perlu boundary domain yang lebih tegas.
  • Tim mulai membagi ownership per area.
  • Anda ingin meningkatkan maintainability tanpa memikul overhead distribusi sistem.
  • Ada kemungkinan beberapa modul akan dipisah di masa depan, tetapi belum sekarang.

Pilih microservices jika:

  • Domain independen sudah jelas dan cukup stabil.
  • Deployment independen memberi manfaat nyata, bukan sekadar preferensi arsitektur.
  • Tim punya kemampuan operasional yang memadai.
  • Workload antar domain berbeda jauh dan scaling per service benar-benar dibutuhkan.
  • Anda siap menangani observability, kontrak API, konsistensi data, dan keamanan antarlayanan.

Sinyal bahwa arsitektur perlu berevolusi

Arsitektur tidak perlu diganti hanya karena codebase membesar. Evolusi diperlukan ketika ada gejala yang konsisten dan berulang.

Sinyal dari monolith ke modular monolith

  • Perubahan kecil sering menyentuh banyak area yang seharusnya tidak terkait.
  • Sulit menentukan owner fitur karena semua area saling campur.
  • Test suite makin lambat karena tidak ada isolasi modul.
  • Bug sering muncul akibat akses lintas domain yang tidak terkendali.

Sinyal dari modular monolith ke microservices

  • Modul tertentu membutuhkan ritme deploy berbeda secara konsisten.
  • Workload modul tertentu jauh lebih tinggi dan boros jika diskalakan bersama seluruh aplikasi.
  • Kegagalan satu area perlu diisolasi secara operasional.
  • Ownership tim per domain sudah jelas dan disiplin kontrak internal sudah terbukti.
  • Boundary modul cukup stabil dan dependency lintas modul sudah rendah.

Kesalahan keputusan yang sering terjadi

  • Menganggap microservices sebagai solusi maintainability default. Jika boundary domain buruk, masalah tetap ada, hanya tersebar ke banyak service.
  • Menggunakan ukuran trafik sebagai satu-satunya alasan. Trafik tinggi tidak otomatis berarti perlu microservices.
  • Mengabaikan kemampuan operasional tim. Arsitektur terdistribusi menuntut kedewasaan proses, bukan hanya skill coding.
  • Memecah berdasarkan tabel atau endpoint, bukan domain. Ini sering menghasilkan coupling yang sulit diputus.
  • Melakukan migrasi besar sekaligus. Strangler pattern atau ekstraksi bertahap biasanya lebih aman.

Rekomendasi praktis yang tidak overengineering

Untuk sebagian besar aplikasi web/backend:

  1. Mulailah dari monolith yang terstruktur.
  2. Begitu kompleksitas domain naik, ubah menjadi modular monolith dengan batas modul yang nyata.
  3. Tambahkan queue, worker, caching, dan background processing sebelum memecah layanan.
  4. Ekstrak ke microservices secara selektif hanya pada domain yang benar-benar mendapat manfaat.

Pendekatan ini memberi jalur evolusi yang murah, realistis, dan lebih aman. Anda tetap bisa bergerak cepat di awal, menjaga maintainability saat tim tumbuh, lalu hanya menambah kompleksitas distribusi ketika nilainya benar-benar terbukti.

Penutup

Memilih monolith, modular monolith, atau microservices sebaiknya didasarkan pada trade-off yang konkret: biaya operasional, kompleksitas deployment, observability, batas domain, pola scaling, dan struktur tim. Tidak ada pilihan yang selalu paling benar untuk semua konteks.

Jika ragu, pilih opsi paling sederhana yang masih menjaga kualitas desain. Dalam praktiknya, itu sering berarti monolith yang rapi atau modular monolith. Microservices sebaiknya menjadi hasil evolusi yang disengaja, bukan keputusan awal yang diambil sebelum masalahnya benar-benar ada.