Jika Anda membangun sistem yang diperkirakan hidup 5-15 tahun, pertanyaan utamanya bukan sekadar "apakah microservices lebih modern?", melainkan arsitektur mana yang paling tahan terhadap perubahan bisnis, rotasi tim, dependency yang menua, dan biaya operasional jangka panjang. Dalam banyak kasus, monolit atau modular monolith adalah pilihan yang lebih rasional daripada microservices, terutama saat kompleksitas domain belum cukup tinggi untuk membenarkan biaya distribusi sistem.

Ada analogi yang berguna dari dunia komponen lama: industri tertentu tetap membutuhkan semiconductor legacy karena sistem fisik mereka hidup jauh lebih lama daripada siklus inovasi komponen. Di software, situasinya mirip. Sistem internal, platform operasional, ERP, billing, manufaktur, atau integrasi B2B sering harus bertahan lama, bahkan ketika framework, library, database driver, dan pola arsitektur di sekitarnya terus berubah. Karena itu, memilih arsitektur untuk sistem umur panjang harus fokus pada kemudahan perawatan, batas modul yang jelas, risiko dependency usang, dan biaya perubahan selama bertahun-tahun.

Monolit, modular monolith, dan microservices: bedanya apa secara praktis?

Monolit

Monolit adalah aplikasi yang dibangun, diuji, dan di-deploy sebagai satu unit. Semua domain bisnis, antarmuka, dan integrasi berjalan di dalam satu proses atau satu artefak deploy.

  • Kelebihan: sederhana dioperasikan, mudah di-debug, observability lebih murah, transaksi lintas modul lebih mudah, dan produktif untuk tim kecil-menengah.
  • Kekurangan: jika struktur kode buruk, coupling cepat membesar; satu perubahan bisa memengaruhi area lain; build dan deploy bisa makin berat seiring waktu.

Modular monolith

Modular monolith tetap satu aplikasi deployable, tetapi kode dibagi ke dalam modul dengan batas yang tegas: API internal, aturan dependensi, dan kepemilikan domain jelas. Ini bukan sekadar folder per fitur; ini adalah monolit dengan disiplin arsitektur.

  • Kelebihan: mempertahankan kesederhanaan operasional monolit, tetapi mengurangi coupling lewat batas modul.
  • Kekurangan: butuh disiplin tinggi. Jika aturan modul tidak ditegakkan, ia akan turun kelas menjadi monolit besar yang kusut.

Microservices

Microservices memecah sistem menjadi beberapa layanan independen dengan deploy, scaling, dan data boundary terpisah. Ini berguna jika domain, throughput, isolasi kegagalan, atau kebutuhan otonomi tim memang menuntutnya.

  • Kelebihan: independensi deploy, isolasi kegagalan yang lebih baik, kepemilikan tim lebih jelas, dan cocok untuk kebutuhan skala serta perubahan yang sangat berbeda antar-domain.
  • Kekurangan: observability, pengujian, deployment, tracing, retry, network failure, kompatibilitas API, dan konsistensi data jadi jauh lebih mahal.

Untuk sistem umur panjang, apa yang sebenarnya sedang Anda optimalkan?

Pada sistem yang harus hidup lama, tujuan arsitektur biasanya bukan mengejar skalabilitas maksimum sejak hari pertama. Yang lebih penting adalah:

  • Biaya perubahan tahunan, bukan hanya kecepatan rilis bulan ini.
  • Kemampuan tim baru memahami sistem tanpa bergantung pada satu-dua engineer senior.
  • Ketahanan terhadap dependency usang, misalnya library tidak lagi dirawat, runtime naik versi, atau vendor mengganti kontrak integrasi.
  • Kemudahan pengujian dan debugging saat bug terjadi di tahun ke-7, bukan hanya saat MVP.
  • Kemampuan migrasi bertahap tanpa rewrite besar.

Di sinilah monolit yang terstruktur baik sering menang. Sistem yang hidup lama biasanya lebih sering menghadapi perubahan aturan bisnis, pergantian integrasi, dan akumulasi kompleksitas operasional dibanding masalah throughput ekstrem. Jika Anda memecah sistem terlalu dini menjadi microservices, Anda bisa menukar kompleksitas kode dengan kompleksitas distribusi yang jauh lebih sulit dirawat dalam jangka panjang.

Trade-off utama: coupling, batas modul, dan deployability

Coupling: bukan soal jumlah service, tetapi arah dependensi

Kesalahan umum adalah menganggap microservices otomatis mengurangi coupling. Kenyataannya, Anda bisa memiliki:

  • Monolit dengan coupling rendah jika batas modul, kontrak, dan ownership jelas.
  • Microservices dengan coupling tinggi jika setiap alur bisnis membutuhkan panggilan sinkron ke banyak service.

Coupling yang paling berbahaya untuk sistem umur panjang adalah coupling yang tersembunyi: shared database, shared enum lintas domain, copy-paste model bisnis, atau workflow yang diam-diam bergantung pada urutan side effect di banyak tempat.

Prinsip praktis: jika dua area bisnis sering berubah bersama, memisahkannya ke service terpisah terlalu cepat sering menambah biaya koordinasi tanpa manfaat nyata.

Batas modul: ukur dari bahasa bisnis, bukan struktur teknis

Batas modul yang baik biasanya mengikuti domain atau subdomain, misalnya:

  • Billing
  • Inventory
  • Order Management
  • User Access
  • Reporting

Batas yang buruk biasanya dibangun dari lapisan teknis seperti controllers, services, atau repositories tanpa mempertimbangkan aturan bisnis. Hasilnya, satu use case menembus banyak package dan dependensi silang sulit dihindari.

Deployability: independen deploy itu mahal

Deploy independen terdengar ideal, tetapi ada harga yang harus dibayar:

  • pipeline CI/CD per service,
  • versioning API,
  • backward compatibility,
  • telemetri lintas service,
  • manajemen rahasia dan konfigurasi,
  • rollback yang tidak selalu atomik.

Jika kebutuhan deploy independen belum nyata, modular monolith sering memberi nilai terbaik: Anda tetap punya pemisahan domain yang rapi, tetapi deploy tetap sederhana.

Biaya observability dan operasional yang sering diremehkan

Pada monolit, Anda biasanya cukup melihat log, metrik aplikasi, dan trace internal proses. Pada microservices, biaya observability meningkat tajam karena Anda perlu:

  • correlation ID lintas request,
  • distributed tracing,
  • standarisasi log antar-service,
  • dashboard health dan dependency,
  • alerting untuk timeout, retry storm, dan queue backlog.

Masalahnya bukan sekadar alat, tetapi juga disiplin operasional. Tanpa standar yang konsisten, microservices membuat debugging jauh lebih lambat. Bug yang pada monolit bisa ditelusuri dengan satu stack trace, pada sistem terdistribusi bisa berubah menjadi kombinasi timeout, retry, event duplikat, dan data tidak sinkron.

Jika tim Anda belum punya praktik observability yang matang, microservices sering memperbesar area masalah lebih cepat daripada memperbesar nilai bisnis.

Skill tim dan umur organisasi lebih penting daripada tren arsitektur

Pilihan arsitektur harus cocok dengan kemampuan tim saat ini dan kemampuan tim masa depan. Sistem umur panjang hampir pasti akan diwariskan ke engineer yang berbeda. Karena itu, arsitektur yang terlalu bergantung pada keahlian khusus bisa menjadi beban.

Kapan monolit atau modular monolith lebih realistis?

  • Tim backend relatif kecil.
  • Belum ada SRE/platform team yang kuat.
  • Masih satu domain inti dengan beberapa subdomain pendukung.
  • Banyak transaksi bisnis perlu konsistensi kuat.
  • Lebih sering butuh perubahan fitur daripada isolasi scaling per domain.

Kapan microservices layak dipertimbangkan serius?

  • Beberapa domain sudah jelas terpisah secara bisnis dan operasional.
  • Rilis tiap domain perlu independen karena ritme perubahan sangat berbeda.
  • Beban kerja antar-domain sangat berbeda dan butuh scaling terpisah.
  • Tim sudah mampu mengelola API contract, observability, incident response, dan integrasi asinkron.
  • Kegagalan satu domain harus benar-benar tidak menjatuhkan domain lain.

Risiko dependency usang pada sistem 5-15 tahun

Sistem berumur panjang hampir pasti menghadapi masalah ini:

  • framework utama berubah arah,
  • library tidak dipelihara,
  • database atau broker masuk fase upgrade besar,
  • SDK vendor deprecated,
  • kontrak API pihak ketiga berubah.

Arsitektur yang baik tidak menghilangkan risiko dependency usang, tetapi membatasi radius ledaknya. Modular monolith sangat berguna di sini karena Anda bisa membungkus dependency di dalam modul tertentu, sehingga migrasi library atau integrasi tidak menyebar ke seluruh codebase.

Contoh prinsip implementasi:

  • Jangan biarkan modul domain bergantung langsung pada SDK vendor.
  • Buat adapter per integrasi eksternal.
  • Definisikan kontrak internal yang stabil.
  • Minimalkan shared utility yang diam-diam mengikat semua modul ke framework tertentu.

Dengan pola ini, ketika suatu dependency menjadi usang, Anda bisa mengganti implementasinya di tepi sistem tanpa membongkar logika domain utama.

Struktur modular monolith yang sehat

Berikut contoh struktur modular monolith yang lebih tahan lama daripada monolit tradisional berbasis lapisan teknis:

src/
  modules/
    billing/
      application/
      domain/
      infrastructure/
      api/
    orders/
      application/
      domain/
      infrastructure/
      api/
    inventory/
      application/
      domain/
      infrastructure/
      api/
  shared/
    kernel/
    logging/
    auth/

Prinsip pentingnya:

  • domain berisi aturan bisnis inti.
  • application mengorkestrasi use case.
  • infrastructure berisi database, message broker, atau SDK eksternal.
  • api adalah pintu masuk modul, bukan akses bebas ke semua class.

Jika memungkinkan, tegakkan aturan bahwa modul lain hanya boleh berinteraksi lewat API internal modul, bukan mengimpor detail implementasi langsung.

Contoh kontrak internal antar-modul

// orders/api/ReserveInventory.java
public interface ReserveInventory {
    ReservationResult reserve(String orderId, List<OrderItem> items);
}

// inventory/api/InventoryApi.java
public class InventoryApi implements ReserveInventory {
    @Override
    public ReservationResult reserve(String orderId, List<OrderItem> items) {
        // orkestrasi ke domain inventory
        return ReservationResult.success();
    }
}

Contoh ini sederhana, tetapi idenya penting: Orders tidak tahu tabel, repository, atau detail implementasi Inventory. Saat suatu hari modul inventory dipisah menjadi service terpisah, kontrak ini menjadi titik awal yang stabil untuk ekstraksi.

Matriks keputusan: monolit, modular monolith, atau microservices?

KriteriaMonolitModular MonolithMicroservices
Ukuran timKecilKecil-menengahMenengah-besar dengan ownership jelas
Kompleksitas domainRendah-menengahMenengah-tinggiTinggi dan terpisah jelas
Kebutuhan deploy independenRendahRendah-menengahTinggi
Konsistensi transaksiMudahMudah-menengahLebih sulit, sering perlu pendekatan eventual consistency
Biaya observabilityRendahRendah-menengahTinggi
Risiko coupling tersembunyiTinggi jika struktur burukTerkendali jika boundary ditegakkanTinggi jika banyak panggilan sinkron lintas service
Skalabilitas operasionalTerbatasCukup untuk banyak kasusTinggi, tetapi mahal
Ketahanan untuk migrasi bertahapRendah jika tidak modularTinggiTinggi, tapi biaya awal besar

Ringkasan praktis:

  • Pilih monolit biasa jika sistem masih sederhana dan tim kecil, tetapi tetap jaga struktur domain sejak awal.
  • Pilih modular monolith sebagai default aman untuk banyak sistem umur panjang.
  • Pilih microservices hanya jika kebutuhan organisasional dan teknisnya nyata, bukan karena asumsi masa depan.

Sinyal bahwa modular monolith sudah cukup

  • Sebagian besar perubahan masih menyentuh domain yang saling terkait.
  • Tim belum kesulitan dengan throughput aplikasi secara keseluruhan.
  • Build dan deploy masih bisa diterima dalam workflow harian.
  • Masalah utama adalah struktur kode dan batas domain, bukan bottleneck infrastruktur terdistribusi.
  • Anda ingin menjaga opsi ekstraksi service di masa depan tanpa membayar biaya distribusi sekarang.

Dalam situasi ini, memperketat boundary modul biasanya memberi hasil lebih besar daripada memecah service.

Sinyal bahwa microservices mulai layak

  • Satu modul butuh lifecycle deploy jauh lebih cepat atau lebih ketat dibanding modul lain.
  • Satu domain memiliki pola beban yang sangat berbeda, misalnya komputasi berat atau traffic sangat tinggi.
  • Kegagalan atau maintenance satu area harus diisolasi dari area lain.
  • Tim sudah terpecah secara stabil berdasarkan domain, bukan hanya berdasarkan komponen teknis.
  • Kontrak antar-domain sudah cukup matang dan tidak berubah liar setiap minggu.

Jika sinyal ini belum jelas, microservices sering menjadi bentuk over-engineering.

Langkah migrasi bertahap tanpa over-engineering

Strategi yang paling aman untuk sistem umur panjang adalah mulai dari monolit yang dimodularisasi, lalu ekstrak service hanya ketika ada tekanan nyata.

1. Bersihkan boundary di dalam monolit lebih dulu

  • Kelompokkan kode berdasarkan domain.
  • Hilangkan akses silang langsung ke repository atau tabel modul lain.
  • Buat API internal per modul.
  • Pisahkan integrasi eksternal ke adapter.

2. Ukur dependensi sebelum memecah

Lihat modul mana yang:

  • paling sering berubah,
  • punya bottleneck performa sendiri,
  • paling banyak ketergantungan eksternal,
  • punya pola operasional berbeda.

Jangan ekstrak berdasarkan intuisi semata. Ekstrak berdasarkan data perubahan dan kebutuhan operasional.

3. Gunakan komunikasi internal yang meniru kontrak service

Walau masih monolit, gunakan antarmuka, command handler, atau event internal agar transisi ke proses terpisah lebih mudah nanti.

// application layer
public class PlaceOrderHandler {
    private final ReserveInventory reserveInventory;
    private final CreateInvoice createInvoice;

    public void handle(PlaceOrderCommand cmd) {
        reserveInventory.reserve(cmd.orderId(), cmd.items());
        createInvoice.create(cmd.orderId(), cmd.total());
    }
}

Di tahap awal, implementasi masih in-process. Saat perlu diekstrak, kontrak ini bisa diganti dengan HTTP, gRPC, atau messaging tanpa membongkar seluruh use case.

4. Ekstrak satu modul yang paling independen lebih dulu

Biasanya kandidat awal yang lebih aman adalah modul dengan:

  • batas domain jelas,
  • sedikit transaksi sinkron lintas domain,
  • integrasi eksternal yang kompleks,
  • beban scaling berbeda.

Contohnya: notifikasi, reporting, search indexing, atau dokumen/invoice generation. Hindari mengekstrak domain inti yang masih terikat ketat pada banyak transaksi sampai boundary benar-benar matang.

5. Pertahankan satu sumber kebenaran data

Kesalahan umum saat migrasi adalah tetap berbagi database antar-service. Ini memang terasa cepat di awal, tetapi mengaburkan ownership dan membuat evolusi schema berbahaya. Jika harus menunda pemisahan data, anggap itu sebagai langkah transisi dengan batas waktu dan dokumentasi eksplisit.

Kesalahan yang paling sering terjadi

  • Memecah service berdasarkan layer teknis alih-alih domain bisnis.
  • Menggunakan shared database permanen antar-service.
  • Terlalu banyak komunikasi sinkron sehingga satu request bergantung ke banyak service.
  • Tidak menegakkan boundary modul pada modular monolith.
  • Mengadopsi event-driven terlalu dini tanpa kebutuhan yang jelas dan tanpa observability memadai.
  • Mengabaikan upgrade path dependency sampai biaya migrasi menjadi terlalu besar.

Debugging dan maintainability: siapa yang menang dalam jangka panjang?

Untuk banyak organisasi, maintainability jangka panjang lebih sering ditentukan oleh kejelasan boundary, kualitas pengujian, dan kemampuan tracing perubahan daripada oleh pola arsitektur yang paling fashionable.

Monolit yang baik biasanya unggul pada:

  • debugging lebih cepat,
  • refactor lintas domain lebih murah,
  • setup developer lebih sederhana,
  • pengujian integrasi lebih langsung.

Microservices yang baik unggul pada:

  • isolasi domain dan tim,
  • evolusi teknologi per layanan,
  • scaling spesifik domain,
  • pengurangan blast radius jika dirancang benar.

Namun perlu digarisbawahi: microservices yang dirancang setengah matang sering justru lebih sulit dirawat daripada modular monolith yang disiplin.

Rekomendasi praktis untuk tech lead

  1. Gunakan modular monolith sebagai default untuk sistem yang diharapkan hidup 5-15 tahun, kecuali ada alasan kuat untuk distribusi lebih awal.
  2. Desain boundary domain sejak awal, meski deploy masih satu artefak.
  3. Bungkus dependency eksternal di adapter agar risiko dependency usang tidak menyebar ke domain inti.
  4. Investasikan pada test, logging, dan dokumentasi kontrak internal sebelum memikirkan pemecahan service.
  5. Ekstrak microservice berdasarkan tekanan nyata: deployability, scale, isolation, atau ownership tim yang jelas.

Penutup

Memilih monolit atau modular monolith untuk sistem umur panjang pada dasarnya adalah keputusan tentang biaya perubahan yang berulang. Seperti komponen legacy di industri hardware yang tetap dibutuhkan karena umur sistemnya panjang, software juga perlu arsitektur yang menghormati kenyataan umur pakai, ketergantungan jangka panjang, dan keterbatasan organisasi.

Jika Anda belum punya alasan operasional yang kuat untuk distribusi, modular monolith biasanya memberi titik keseimbangan terbaik: struktur domain lebih sehat daripada monolit biasa, tetapi tanpa biaya observability, jaringan, dan koordinasi yang datang bersama microservices. Bangun boundary yang baik sekarang, lalu biarkan kebutuhan nyata menentukan apakah suatu modul pantas keluar menjadi service terpisah.