Jawaban singkatnya: untuk banyak organisasi dengan skala menengah, modular monolith di Spring Boot masih cukup untuk multi-tim selama batas modul jelas, dependensi dijaga, deployment bersama masih bisa diterima, dan bottleneck utama belum berasal dari kebutuhan otonomi operasional per domain. Berpindah ke microservices terlalu dini sering menambah kompleksitas jaringan, observability, deployment, dan koordinasi tanpa menyelesaikan masalah inti.

Masalahnya bukan sekadar ukuran codebase, melainkan di mana gesekan utama terjadi: apakah tim saling mengganggu karena batas domain kabur, apakah satu rilis menahan semua perubahan, apakah kebutuhan skala tiap domain sangat berbeda, dan apakah kegagalan di satu area sering menjatuhkan sistem lain. Artikel ini membahas cara menilai kapan modular monolith masih efisien, kapan mulai menjadi bottleneck, dan bagaimana menyiapkan struktur Spring Boot yang tetap sehat sebelum mempertimbangkan microservices.

Mengapa modular monolith sering cukup lebih lama dari yang dibayangkan

Modular monolith adalah satu aplikasi yang dideploy sebagai satu unit, tetapi secara internal dipisah menjadi modul-modul dengan batas domain yang jelas. Berbeda dari monolith tradisional yang cenderung acak, modular monolith menekankan encapsulation, kontrak antarmodul, dan disiplin dependensi.

Pendekatan ini sering cukup untuk multi-tim karena memberi beberapa keuntungan praktis:

  • Sederhana secara operasional: satu proses deployment, satu artefak utama, lebih sedikit moving parts.
  • Latensi antarmodul rendah: komunikasi in-process lebih sederhana dibanding panggilan jaringan.
  • Transaksi lebih mudah: konsistensi data antarbagian masih bisa dikelola tanpa distributed transaction.
  • Debugging lebih langsung: stack trace, log, dan profiling biasanya lebih mudah ditelusuri.
  • Biaya platform lebih rendah: tidak perlu langsung membangun service discovery, gateway, distributed tracing penuh, retry policy lintas service, dan orkestrasi kompleks.

Untuk tim engineering level menengah, ini penting: banyak masalah yang tampak seperti “sudah waktunya microservices” sebenarnya berasal dari desain modul yang buruk, ownership yang kabur, atau proses delivery yang belum rapi. Memecah aplikasi tidak otomatis memperbaiki itu.

Cara menilai apakah modular monolith Spring Boot masih efisien

1. Batas domain masih bisa dibuat jelas

Jika area seperti catalog, order, billing, dan identity masih bisa dipisahkan dengan API internal yang jelas, modular monolith masih masuk akal. Kunci utamanya bukan folder terpisah, tetapi:

  • modul tidak mengakses tabel atau repository modul lain secara sembarangan,
  • komunikasi lewat service interface atau domain event internal,
  • aturan bisnis utama tinggal di modul domain, bukan tersebar di controller atau util bersama.

Kalau tim masih bisa menjelaskan dengan mudah “siapa pemilik data dan aturan bisnis untuk fitur ini”, struktur monolith biasanya masih bisa dipertahankan.

2. Deployment bersama belum menjadi hambatan utama

Satu ciri penting modular monolith: seluruh aplikasi dideploy bersama. Ini masih efisien bila:

  • frekuensi rilis cukup teratur dan tidak menimbulkan antrean panjang,
  • perubahan di satu modul jarang memaksa retest besar-besaran di seluruh sistem,
  • pipeline CI/CD masih selesai dalam waktu yang dapat diterima,
  • tim dapat menyepakati jadwal rilis tanpa konflik operasional besar.

Jika setiap perubahan kecil harus menunggu sinkronisasi banyak tim, atau satu modul yang sensitif membuat semua rilis tertahan, itu tanda deployment bersama mulai menjadi beban.

3. Kebutuhan skala belum sangat berbeda per domain

Microservices sering masuk akal ketika tiap domain punya pola beban sangat berbeda. Contoh: modul pencarian perlu autoscaling agresif, sedangkan billing butuh throughput rendah tetapi audit tinggi. Namun bila sebagian besar trafik masih bisa ditangani dengan menskalakan aplikasi Spring Boot secara horizontal sebagai satu unit, modular monolith tetap efisien.

Pertanyaan praktisnya: apakah kita benar-benar butuh skala independen, atau hanya butuh tuning performa dan query yang lebih baik? Banyak bottleneck berasal dari akses database, cache yang belum tepat, atau desain endpoint yang terlalu gemuk, bukan karena arsitektur monolith itu sendiri.

4. Observability masih bisa dikelola tanpa pemisahan service

Dalam modular monolith, observability sering lebih sederhana karena request flow masih berada dalam satu proses. Jika tim masih bisa menjawab pertanyaan berikut dengan cepat, monolith biasanya belum jadi masalah:

  • request mana yang lambat,
  • modul mana yang paling sering error,
  • query mana yang mahal,
  • fitur mana yang paling sering memicu retry atau timeout ke sistem eksternal.

Jika akar masalah observability adalah logging yang tidak konsisten, minim metrik bisnis, atau tracing internal yang belum rapi, memecah ke microservices justru memperburuk visibilitas sebelum fondasinya siap.

5. Struktur tim masih bisa disejajarkan dengan ownership modul

Multi-tim tidak otomatis berarti multi-service. Satu aplikasi masih bisa dikerjakan beberapa tim jika setiap tim memiliki modul dengan ownership jelas, kontrak perubahan disepakati, dan review dependensi dijaga. Modular monolith cocok bila koordinasi antartim masih lebih murah daripada biaya operasional tambahan dari microservices.

Tanda modular monolith mulai menjadi bottleneck

Berikut indikator yang lebih konkret bahwa monolith mungkin mulai mencapai batas praktisnya:

1. Perubahan lintas modul terlalu sering dan sulit diprediksi

Jika hampir setiap fitur menyentuh banyak modul, itu bisa berarti batas domain salah, atau ada domain yang sebenarnya pantas dipisah. Gejalanya:

  • PR besar dan sulit direview,
  • regresi di area yang tampaknya tidak terkait,
  • tim sering saling blokir karena kontrak internal tidak stabil.

2. Build, test, dan startup makin mahal secara signifikan

Bukan soal angka absolut, melainkan feedback loop. Jika perubahan kecil di satu modul tetap memicu build dan pengujian luas yang menghambat produktivitas, biaya koordinasi naik. Ini bisa diatasi lebih dulu dengan pemisahan test, modularisasi build, atau optimasi startup, tetapi bila tidak cukup, arsitektur mungkin perlu berubah.

3. Satu domain memerlukan siklus rilis yang jauh lebih cepat atau lebih hati-hati

Misalnya tim fraud atau pembayaran butuh proses validasi, audit, dan rollback yang berbeda jauh dari modul lain. Jika kebutuhan operasional domain-domain tertentu makin berbeda, deploy sebagai satu unit akan terasa kaku.

4. Isolasi kegagalan menjadi kebutuhan nyata

Jika error atau lonjakan beban di satu modul rutin mengganggu modul lain, itu sinyal penting. Dalam modular monolith, Anda masih bisa menambah circuit breaker untuk dependency eksternal, bulkhead pada thread pool tertentu, atau queue asinkron internal. Tetapi jika kebutuhan isolasi menjadi dominan, service boundary mungkin lebih layak.

5. Kepemilikan data tidak lagi bisa dipertahankan di satu basis data bersama

Database bersama sering memudahkan di awal, tetapi menjadi masalah saat banyak tim mengakses tabel satu sama lain, migrasi schema sulit dikoordinasikan, dan optimasi per domain saling mengganggu. Jika kebutuhan kepemilikan data per domain makin kuat, itu salah satu sinyal paling sah untuk memikirkan pemisahan service.

Struktur modul Spring Boot yang praktis

Pada konteks ini, tujuan utamanya bukan membuat banyak proyek kecil demi terlihat modular, melainkan membangun batas teknis yang dapat ditegakkan. Salah satu pendekatan yang umum adalah multi-module build atau setidaknya package by feature dengan aturan dependensi ketat.

Contoh struktur modul

acme-app/
├── app-bootstrap
│   └── src/main/java/com/acme/app/Application.java
├── common
│   └── src/main/java/com/acme/common/
├── identity
│   └── src/main/java/com/acme/identity/
├── catalog
│   └── src/main/java/com/acme/catalog/
├── ordering
│   └── src/main/java/com/acme/ordering/
├── billing
│   └── src/main/java/com/acme/billing/
└── reporting
    └── src/main/java/com/acme/reporting/

Setiap modul sebaiknya memiliki lapisan internal yang jelas, misalnya:

  • api: kontrak yang boleh dipakai modul lain,
  • application: orchestration use case,
  • domain: aturan bisnis inti,
  • infrastructure: JPA repository, adapter eksternal, messaging, dsb.

Contoh kontrak antarmodul

package com.acme.catalog.api;

public interface ProductQueryService {
    ProductSummary getProductById(String productId);
}
package com.acme.ordering.application;

import com.acme.catalog.api.ProductQueryService;

@Service
public class PlaceOrderService {

    private final ProductQueryService productQueryService;

    public PlaceOrderService(ProductQueryService productQueryService) {
        this.productQueryService = productQueryService;
    }

    public OrderResult place(PlaceOrderCommand command) {
        ProductSummary product = productQueryService.getProductById(command.productId());
        // validasi dan aturan bisnis order
        return new OrderResult();
    }
}

Poin pentingnya: modul ordering bergantung pada API modul catalog, bukan langsung ke entity atau repository internalnya. Ini membuat batas modul lebih eksplisit dan lebih mudah diekstrak menjadi service terpisah nanti jika memang perlu.

Kapan memakai event internal

Untuk alur yang tidak harus sinkron, event internal membantu mengurangi coupling. Contohnya setelah order sukses dibuat, modul billing atau reporting dapat merespons event domain. Dalam monolith, event ini masih in-process sehingga lebih sederhana dibanding broker lintas service. Namun tetap perlu kehati-hatian soal transaction boundary dan idempotency jika nanti berkembang menjadi asinkron penuh.

Trade-off utama: modular monolith vs microservices

Biaya operasional

Modular monolith menang pada kesederhanaan. Anda mengelola lebih sedikit pipeline, lebih sedikit artefak, lebih sedikit konfigurasi jaringan, dan lebih sedikit komponen observability terdistribusi. Microservices memberi otonomi lebih, tetapi biaya platform naik: deployment per service, konfigurasi environment, monitoring lintas service, tracing, retry, timeout, dan kompatibilitas kontrak API.

Koordinasi tim

Microservices membantu bila tim benar-benar perlu bergerak independen per domain. Tetapi jika organisasi belum matang pada ownership, contract testing, dan operasional mandiri, hasilnya bisa menjadi distributed monolith: service terpisah, tetapi perubahan tetap saling bergantung dan lebih sulit ditelusuri.

Performa dan latensi

Untuk komunikasi internal yang sering, modular monolith biasanya lebih efisien karena tidak terkena overhead jaringan dan serialisasi. Microservices unggul bila kebutuhan skala tiap domain sangat berbeda atau workload-nya memang terpisah jelas. Namun memecah service bukan solusi otomatis untuk performa buruk.

Maintainability

Monolith modular lebih mudah dipahami secara end-to-end pada skala menengah, selama disiplin modul dijaga. Microservices bisa meningkatkan maintainability bila batas domain matang, tetapi justru menurunkannya bila dipaksakan terlalu dini: duplikasi utilitas, kontrak API rapuh, dan debugging lintas jaringan menjadi lebih rumit.

Matriks keputusan untuk skala menengah

AspekModular MonolithMicroservicesPilih yang mana?
Ukuran timBeberapa tim dengan koordinasi masih terkelolaBanyak tim dengan otonomi tinggi per domainJika sinkronisasi masih murah, monolith modular cukup
DeploymentSatu unit rilisRilis independen per serviceButuh rilis independen yang nyata? microservices lebih cocok
SkalabilitasScale bersama sebagai satu aplikasiScale per serviceJika hotspot hanya sedikit dan masih bisa ditangani tuning, monolith cukup
ObservabilityLebih sederhanaLebih kompleks, perlu tracing lintas serviceJika tooling observability belum matang, jangan buru-buru pecah service
Data ownershipSering berbagi databaseLebih cocok untuk boundary data yang ketatJika konflik schema dan akses silang makin parah, pertimbangkan microservices
Latensi internalRendah, in-processLebih tinggi, network callUntuk workflow sinkron padat, monolith sering lebih efisien
Isolasi kegagalanTerbatas, tetapi bisa dibantu bulkhead dan queueLebih baik jika dirancang benarJika blast radius harus kecil per domain, microservices lebih masuk akal
Kompleksitas platformRendah hingga sedangTinggiSkala menengah biasanya lebih diuntungkan dari kesederhanaan monolith modular

Anti-pattern umum pada Spring Boot modular monolith

1. Modular hanya di nama, tetapi database dan kode tetap saling tembus

Contoh buruk: modul ordering langsung mengakses CatalogRepository atau tabel catalog untuk logika bisnisnya. Secara struktur mungkin tampak modular, tetapi coupling tetap tinggi.

2. Shared module yang terlalu gemuk

Modul common sering berubah menjadi tempat semua hal: DTO, util, entity dasar, validator, bahkan logic bisnis generik. Akibatnya semua modul bergantung pada satu paket pusat yang sulit dirapikan. Gunakan shared module hanya untuk kebutuhan yang benar-benar lintas domain dan stabil.

3. Entity JPA dibagikan lintas modul

Ini memudahkan di awal, tetapi merusak encapsulation. Modul lain seharusnya tidak tahu bentuk persistence internal Anda secara penuh. Lebih aman expose DTO, projection, atau interface domain-level.

4. Synchronous call untuk semua interaksi

Jika setiap modul memanggil modul lain secara sinkron untuk semua pekerjaan, coupling meningkat dan request chain menjadi rapuh. Gunakan event internal untuk kebutuhan yang tidak harus blocking.

5. Memecah ke microservices karena codebase besar, bukan karena kebutuhan boundary

Codebase besar memang menyulitkan, tetapi solusi pertamanya sering adalah memperbaiki modularisasi, pengujian, pipeline, dan ownership. Jika tidak, Anda hanya memindahkan kerumitan ke jaringan.

Checklist evaluasi untuk tim teknis

Gunakan checklist ini setiap kali muncul dorongan “kita harus pindah ke microservices”. Jika sebagian besar jawabannya masih mendukung monolith, jangan pindah dulu.

  1. Apakah batas domain kita sudah jelas di codebase saat ini?
  2. Apakah modul berinteraksi lewat kontrak, bukan akses langsung ke detail internal?
  3. Apakah perubahan fitur mayoritas masih bisa diselesaikan dalam 1-2 modul utama?
  4. Apakah build, test, dan release pipeline masih cukup cepat untuk ritme tim?
  5. Apakah deployment bersama benar-benar masalah bisnis atau hanya ketidaknyamanan engineering?
  6. Apakah ada domain yang butuh scaling sangat berbeda dari domain lain?
  7. Apakah satu modul yang bermasalah sering menjatuhkan modul lain meskipun mitigasi dasar sudah diterapkan?
  8. Apakah ownership data per domain sudah cukup matang untuk dipisah?
  9. Apakah tim siap mengelola observability, alerting, tracing, dan incident response lintas service?
  10. Apakah kita memiliki kebutuhan nyata untuk deploy independen, bukan sekadar asumsi masa depan?

Jika jawaban untuk poin 1-4 masih positif dan poin 6-10 belum mendesak, modular monolith biasanya masih pilihan paling rasional.

Strategi transisi yang lebih aman daripada rewrite besar

Jika Anda mulai melihat batas monolith, hindari rewrite total. Pendekatan yang lebih aman adalah mengeraskan modular monolith lebih dulu, lalu ekstrak hanya domain yang benar-benar membutuhkan pemisahan.

Langkah yang biasanya lebih efektif

  • Tegakkan boundary di codebase: batasi akses package, definisikan API modul, kurangi ketergantungan silang.
  • Perjelas ownership data: walau masih satu database, sepakati tabel mana milik domain mana.
  • Pisahkan alur sinkron dan asinkron: gunakan event untuk proses yang tidak harus blocking.
  • Perkuat observability: tambah log terstruktur, correlation ID, metrik per modul, dan dashboard yang memetakan domain.
  • Ukur hotspot nyata: lihat endpoint, query, atau job yang paling bermasalah sebelum menyimpulkan perlu microservices.
  • Ekstrak service secara selektif: mulai dari domain yang paling independen secara bisnis dan operasional.

Pendekatan ini bekerja karena Anda mengubah arsitektur berdasarkan bottleneck nyata, bukan berdasarkan asumsi bahwa sistem besar pasti harus menjadi microservices.

Kesimpulan

Untuk banyak aplikasi Spring Boot di organisasi skala menengah, modular monolith cukup untuk multi-tim lebih lama daripada yang sering diasumsikan. Selama batas modul jelas, koordinasi masih terkelola, kebutuhan skala belum sangat berbeda per domain, dan deployment bersama belum menjadi hambatan nyata, monolith modular biasanya memberi rasio manfaat-kompleksitas yang lebih baik.

Mulailah dari pertanyaan yang benar: masalah kita ada di boundary domain, proses delivery, performa, atau otonomi operasional? Jika akar masalah belum jelas, berpindah ke microservices terlalu cepat sering hanya membuat sistem lebih mahal dan lebih sulit dioperasikan. Sebaliknya, modular monolith yang dirancang disiplin memberi fondasi yang kuat, dan jika suatu saat perlu dipecah, Anda melakukannya dari posisi yang jauh lebih siap.