Monolit bersih vs microservice bukan pertanyaan tentang arsitektur mana yang lebih modern, melainkan mana yang memberi biaya perubahan paling rendah untuk kondisi tim Anda saat ini. Untuk tim kecil hingga menengah yang mulai tumbuh, keputusan ini biasanya ditentukan oleh tiga hal: seberapa bersih struktur kode, seberapa jelas batas domain, dan seberapa besar beban operasional yang sanggup ditanggung.

Jawaban singkatnya: jika satu codebase masih bisa dipahami, diuji, dan diubah dengan aman oleh tim, monolit modular sering menjadi pilihan terbaik. Jika bottleneck utama sudah bergeser ke ownership lintas tim, kebutuhan deployment independen, isolasi kegagalan, atau perbedaan kebutuhan skala antar domain, barulah microservice layak dipertimbangkan. Namun sebelum memecah sistem, penting memahami bahwa code cleanliness sering lebih menentukan maintainability daripada label arsitekturnya.

Ini relevan dengan konteks studi controlled minimal-pair tentang kebersihan kode dan coding agent: ketika dua implementasi menyelesaikan masalah yang sama, perbedaan struktur, konsistensi penamaan, pemisahan tanggung jawab, dan batas modul dapat memengaruhi kemampuan otomasi untuk menavigasi kode, membuat perubahan aman, dan mengurangi kesalahan. Dampaknya bukan hanya pada AI agent, tetapi juga pada developer manusia: review lebih cepat, debugging lebih terarah, dan perubahan lebih murah. Pelajaran utamanya jelas: arsitektur yang baik tidak menyelamatkan codebase yang kotor, tetapi codebase yang bersih sering menunda kebutuhan microservice lebih lama dari yang dibayangkan.

Mengapa kebersihan kode harus masuk ke diskusi arsitektur

Banyak tim membahas monolit vs microservice terlalu cepat pada level deployment, padahal masalah nyata sering ada di level kode dan batas modul. Jika perubahan sederhana selalu menyentuh banyak file yang tidak terkait, sulit ditest, dan memicu regressions, penyebabnya belum tentu karena monolit. Bisa jadi karena:

  • aturan bisnis tersebar tanpa boundary yang jelas,
  • lapisan aplikasi bocor langsung ke query, transport, dan infrastruktur,
  • nama modul tidak mewakili domain,
  • dependensi antarbagian bersifat siklik,
  • test tidak memberi perlindungan terhadap refactor.

Dalam studi minimal-pair, kode yang lebih bersih cenderung lebih mudah dipahami oleh agent maupun developer karena memiliki surface area yang lebih kecil untuk setiap perubahan. Hal ini berpengaruh langsung pada:

  • Otomasi: tooling, static analysis, generator, dan coding agent lebih mudah memodifikasi bagian yang tepat.
  • Code review: diff menjadi lebih kecil dan fokus pada domain, bukan efek samping struktural.
  • Debugging: alur eksekusi dan batas tanggung jawab lebih jelas.
  • Kecepatan perubahan: risiko perubahan menurun karena coupling lebih rendah.

Kesimpulannya, sebelum bertanya "apakah kita harus pindah ke microservice?", tim perlu bertanya dulu: apakah monolit kita benar-benar modular dan bersih?

Monolit modular: kapan ia cukup, dan kenapa sering lebih efektif

Monolit modular adalah satu aplikasi yang di-deploy sebagai satu unit, tetapi disusun dengan boundary internal yang tegas. Setiap modul mewakili domain atau capability, memiliki API internal yang jelas, dan sebisa mungkin menghindari akses langsung ke detail modul lain.

Kelebihan teknis monolit modular

  • Observability lebih sederhana: tracing request, profiling, dan debugging tidak perlu melintasi jaringan.
  • Transaksi lebih mudah: konsistensi data dapat dijaga dalam satu boundary database atau transaction manager.
  • Latensi lebih rendah: pemanggilan antar modul terjadi di memori proses, bukan melalui network hop.
  • Deployment lebih sederhana: lebih sedikit pipeline, registry, secret, dan artefak yang perlu dikelola.
  • Biaya operasional rendah: logging, alerting, auth antar komponen, dan service discovery jauh lebih ringan.
  • Refactor domain lebih aman: perubahan boundary internal belum perlu kontrak jaringan yang kaku.

Risiko jika monolit tidak dijaga kebersihannya

Monolit gagal bukan karena "mono", melainkan karena berubah menjadi big ball of mud. Gejalanya antara lain:

  • setiap fitur baru menyentuh banyak area yang tidak terkait,
  • dependency graph sulit dipahami,
  • waktu build dan test makin lama,
  • tim saling bertabrakan di area yang sama,
  • satu bug kecil menyebabkan dampak luas.

Dalam kondisi seperti ini, memecah menjadi microservice memang tampak menarik. Tetapi jika batas domain belum bersih, Anda hanya memindahkan coupling internal menjadi coupling lewat jaringan, yang biasanya lebih sulit dipantau dan lebih mahal ditangani.

Contoh struktur monolit modular

Struktur berikut menunjukkan cara memisahkan domain tanpa harus langsung memecah deployment:

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

Poin pentingnya bukan nama folder, tetapi aturan dependensinya: modul orders tidak boleh membaca tabel atau repository modul billing secara langsung. Interaksi dilakukan lewat API internal, command, event, atau service interface yang jelas.

Microservice: kapan benar-benar membantu

Microservice masuk akal ketika masalah utama tim bukan lagi struktur internal satu codebase, melainkan koordinasi lintas tim dan kebutuhan isolasi sistem. Ini berarti keuntungan utamanya ada pada independensi organisasi dan operasional, bukan sekadar pemisahan kode.

Manfaat microservice yang nyata

  • Deployment independen: tim dapat merilis perubahan pada satu layanan tanpa merilis seluruh aplikasi.
  • Ownership lebih jelas: satu tim bertanggung jawab atas satu domain, termasuk runtime dan kualitas operasionalnya.
  • Isolasi skala: service dengan beban tinggi dapat diskalakan terpisah dari service lain.
  • Isolasi kegagalan terbatas: jika dirancang baik, masalah di satu service tidak selalu menjatuhkan semuanya.
  • Teknologi bisa lebih fleksibel: domain tertentu dapat memilih storage atau runtime yang paling cocok, jika manfaatnya melebihi biayanya.

Biaya teknis yang sering diremehkan

  • Latensi jaringan: call lokal berubah menjadi remote call dengan timeout, retry, dan kemungkinan partial failure.
  • Kegagalan terdistribusi: service bisa timeout, pesan bisa terlambat, idempotency harus dipikirkan, dan state bisa sementara tidak sinkron.
  • Observability lebih mahal: perlu korelasi log, distributed tracing, metrics per service, dan alert yang tidak berisik.
  • Kompleksitas deployment: lebih banyak pipeline CI/CD, lebih banyak konfigurasi, dan lebih banyak permukaan kegagalan.
  • Kontrak API harus disiplin: versioning, backward compatibility, schema evolution, dan consumer impact menjadi pekerjaan rutin.
  • Data consistency lebih sulit: transaksi lintas service tidak lagi gratis; sering perlu pendekatan eventual consistency.

Karena itu, microservice bukan cara gratis untuk memperbaiki maintainability. Ia hanya efektif jika Anda memang membutuhkan pemisahan operasional dan tim, serta punya disiplin engineering untuk mengelola kompleksitas distribusi.

Hubungan code cleanliness, coding agent, dan keputusan arsitektur

Konteks studi minimal-pair tentang kebersihan kode memberi pelajaran praktis untuk tim modern yang mulai memakai coding agent, code generation, atau automated refactoring. Sistem yang bersih memiliki representasi domain yang lebih eksplisit. Itu membuat perubahan lebih dapat diprediksi, baik oleh manusia maupun alat otomatis.

Apa yang membuat codebase lebih ramah untuk otomasi

  • Boundary jelas: satu perubahan domain tidak memerlukan traversal ke banyak lapisan yang saling bocor.
  • Penamaan konsisten: entity, use case, repository, dan handler mudah dikenali.
  • Alur data eksplisit: input, validasi, aturan bisnis, dan efek samping mudah dilacak.
  • Test terfokus: ada perlindungan untuk mengubah implementasi tanpa merusak perilaku.
  • Kontrak stabil: API internal atau eksternal memiliki bentuk yang jelas dan dapat diverifikasi.

Ini penting untuk keputusan monolit vs microservice karena codebase yang bersih memberi dua keuntungan sekaligus:

  1. Jika tetap monolit, tim masih bisa bergerak cepat karena perubahan lokal dan review lebih sederhana.
  2. Jika nanti dipecah menjadi microservice, kandidat boundary service lebih mudah diidentifikasi karena modul internal sudah rapi.

Sebaliknya, jika codebase kotor, migrasi ke microservice sering hanya memecah kebingungan menjadi banyak repository, banyak pipeline, dan banyak titik kegagalan.

Kerangka keputusan: tetap di monolit atau pecah ke service

Gunakan kerangka berikut saat tim mulai scale. Fokusnya bukan pada opini, tetapi pada sinyal teknis yang bisa diamati.

Tetap di monolit modular jika

  • sebagian besar perubahan masih bisa dilakukan dalam satu repo tanpa konflik besar antar tim,
  • waktu build dan test masih dapat diterima untuk ritme kerja harian,
  • deployment bersama belum menjadi bottleneck utama,
  • domain masih sering berubah dan boundary belum stabil,
  • kebutuhan konsistensi transaksional antar modul masih tinggi,
  • tim platform atau SRE belum siap menanggung overhead sistem terdistribusi,
  • masalah utama berasal dari struktur kode yang kotor, bukan skala operasional per domain.

Pertimbangkan memecah service jika

  • satu domain memiliki laju perubahan dan siklus rilis yang jelas berbeda dari domain lain,
  • ada tim yang benar-benar bisa memiliki service secara end-to-end,
  • beban trafik atau resource pada satu capability jauh lebih tinggi daripada yang lain,
  • kegagalan pada satu area harus diisolasi lebih ketat dari area lain,
  • batas domain sudah cukup stabil dan dapat dipisahkan dengan kontrak API yang masuk akal,
  • observability, CI/CD, secret management, dan incident response sudah cukup matang,
  • latensi tambahan dan eventual consistency dapat diterima oleh proses bisnis.

Pertanyaan keputusan yang sebaiknya dijawab tertulis

  1. Masalah apa yang ingin diselesaikan: konflik tim, skala, reliabilitas, atau hanya kode yang sulit dipahami?
  2. Jika satu domain dipisah, siapa owner operasionalnya?
  3. Apakah komunikasi antar domain cocok sebagai API/event, atau sebenarnya masih butuh transaksi kuat?
  4. Apakah kita sudah punya logging, metrics, tracing, dan alert yang cukup untuk sistem terdistribusi?
  5. Berapa banyak call sinkron baru yang akan tercipta, dan apa dampaknya pada latency?
  6. Bagaimana strategi retry, timeout, idempotency, dan dead-letter jika memakai messaging?

Contoh evolusi yang aman: dari modul internal ke service terpisah

Pendekatan yang lebih aman adalah memulai dari monolit modular, lalu mengekstrak service berdasarkan boundary yang sudah terbukti. Misalnya modul billing awalnya dipanggil sebagai interface internal:

public interface BillingGateway {
    Invoice createInvoice(Order order);
}

public class BillingModuleGateway implements BillingGateway {
    public Invoice createInvoice(Order order) {
        // aturan billing di dalam monolit
    }
}

Controller atau use case di modul lain hanya mengenal BillingGateway, bukan detail implementasi. Saat kebutuhan operasional berubah, implementasi gateway dapat diganti ke HTTP atau messaging tanpa mengubah domain caller secara besar-besaran:

public class BillingHttpGateway implements BillingGateway {
    public Invoice createInvoice(Order order) {
        // panggil service billing melalui HTTP client
        // tambahkan timeout, retry terbatas, dan mapping error
    }
}

Pola ini bekerja karena Anda memisahkan boundary domain dari mekanisme transport. Artinya, pemecahan service menjadi langkah evolusi, bukan rewrite total.

Catatan penting saat ekstraksi

  • Mulai dari domain dengan coupling relatif rendah.
  • Hindari shared database jika tujuan Anda adalah independensi nyata.
  • Ukur dependency call yang muncul setelah ekstraksi; jangan hanya memindahkan kode.
  • Tentukan source of truth data per service sejak awal.

Trade-off teknis yang paling menentukan

1. Biaya operasional

Monolit biasanya menang pada kesederhanaan. Microservice menambah kebutuhan untuk gateway, registry, secret rotation, deployment orchestration, monitoring, dan incident triage. Untuk tim kecil-menengah, biaya ini sering lebih signifikan daripada dugaan awal.

2. Observability

Di monolit, stack trace dan satu set log sering cukup untuk melacak masalah. Di microservice, satu request bisa melewati banyak service. Tanpa correlation ID dan distributed tracing, debugging menjadi lambat dan menyesatkan.

Praktik minimal yang layak untuk microservice: correlation ID di setiap request, structured logging, metrics per endpoint/queue, dan tracing untuk path kritis.

3. Deployment dan release

Monolit merilis semuanya bersama, yang bisa menjadi kelemahan jika domain berubah dengan ritme sangat berbeda. Namun release tunggal juga menyederhanakan koordinasi. Microservice memberi independensi, tetapi hanya bernilai jika service memang memiliki lifecycle yang berbeda dan tim mampu mengelola kontrak perubahan.

4. Ownership tim

Microservice efektif jika ada owner yang jelas. Tanpa itu, Anda mendapatkan banyak service "yatim" yang bug-nya dilempar ke banyak pihak. Jika organisasi belum punya ownership yang stabil, monolit modular sering lebih sehat.

5. Latency dan failure mode

Call antarmodul dalam monolit hampir selalu lebih murah daripada panggilan jaringan. Di microservice, timeout, retry berantai, dan dependency yang lambat dapat menurunkan performa seluruh alur bisnis. Desain API sinkron berlebihan adalah sumber masalah umum.

6. Maintainability jangka panjang

Maintainability tidak otomatis naik setelah migrasi ke microservice. Ia naik jika setiap service punya boundary jelas, kontrak sempit, test yang baik, dan owner yang bertanggung jawab. Jika tidak, yang terjadi justru fragmentation: banyak repo kecil, pola tak konsisten, dan debugging lebih sulit.

Anti-pattern umum

Monolith first, but messy forever

Tim berkata memilih monolit demi kesederhanaan, tetapi tidak pernah membangun boundary internal. Hasilnya adalah coupling tinggi dan perubahan makin mahal. Solusinya bukan langsung microservice, melainkan modulasi, pengurangan akses lintas domain, dan penguatan test.

Distributed monolith

Ini anti-pattern klasik: sistem dipecah menjadi banyak service, tetapi semuanya saling bergantung sinkron secara ketat, berbagi schema mental yang sama, bahkan kadang berbagi database. Anda mendapat semua biaya microservice tanpa manfaat independensi.

Shared database antar service

Jika beberapa service menulis ke tabel yang sama, boundary domain menjadi ilusi. Perubahan schema menjadi koordinasi lintas service, ownership kabur, dan debugging data race lebih sulit.

Memecah service berdasarkan layer teknis, bukan domain

Contohnya membuat service "user-service", "notification-service", "database-service" tanpa analisis alur bisnis. Ini sering menghasilkan chatty calls dan domain logic yang tersebar.

Migrasi besar sekaligus

Rewrite total ke microservice meningkatkan risiko delivery berhenti lama, regresi membesar, dan observability tertinggal. Evolusi bertahap hampir selalu lebih aman.

Checklist evaluasi untuk tim kecil-menengah

Gunakan checklist ini sebelum memutuskan.

Checklist monolit modular sehat

  • Apakah modul domain utama sudah punya boundary dan API internal yang jelas?
  • Apakah perubahan fitur biasanya tetap lokal pada satu atau dua modul?
  • Apakah test cukup cepat dan memberi perlindungan terhadap refactor?
  • Apakah akses database lintas domain bisa dibatasi?
  • Apakah naming, struktur folder, dan alur request konsisten?
  • Apakah code review masih bisa fokus pada domain, bukan membahas efek samping struktural?

Checklist kesiapan microservice

  • Apakah ada domain yang layak dimiliki satu tim secara penuh?
  • Apakah kebutuhan deploy, scale, atau SLA domain itu benar-benar berbeda?
  • Apakah batas data dan kontrak API cukup jelas?
  • Apakah ada observability dasar: logs terstruktur, metrics, tracing, correlation ID?
  • Apakah tim siap menangani timeout, retry, idempotency, circuit breaking, dan dead-letter?
  • Apakah biaya infrastruktur dan operasional sudah diperhitungkan, bukan diabaikan?

Rekomendasi praktis

Untuk kebanyakan tim yang baru mulai tumbuh, strategi paling rasional adalah:

  1. Rapikan monolit lebih dulu: perjelas boundary domain, kurangi coupling, dan tambahkan test pada alur kritis.
  2. Gunakan modul sebagai unit ownership: tetapkan siapa yang bertanggung jawab pada area domain tertentu, walau masih dalam satu repo.
  3. Ukur bottleneck nyata: apakah masalahnya build lambat, deploy tersendat, konflik tim, atau hotspot trafik?
  4. Ekstrak service secara bertahap: pilih domain yang boundary-nya stabil dan manfaat operasionalnya jelas.
  5. Bangun observability sebelum skala distribusi: jangan menunggu insiden besar untuk mulai menata tracing dan metrics.

Jika dikaitkan kembali ke studi tentang kebersihan kode dan coding agent, pesannya konsisten: maintainability tumbuh dari struktur yang jelas, bukan dari jumlah service. Monolit bersih sering lebih produktif, lebih mudah diautomasi, dan lebih murah dioperasikan. Microservice tepat ketika kebutuhan organisasi dan operasional benar-benar menuntut pemisahan. Jadi, jangan mulai dari pertanyaan "arsitektur apa yang keren", tetapi dari pertanyaan yang lebih teknis: di mana biaya perubahan kita paling besar, dan boundary mana yang benar-benar stabil untuk dipisahkan?