Kapan mengurangi dependensi lebih murah daripada pecah service? Jawaban singkatnya: ketika kebutuhan reuse belum cukup stabil, tim masih kecil atau menengah, batas domain belum jelas, dan biaya koordinasi lintas service lebih besar daripada manfaat isolasi yang didapat. Dalam situasi seperti ini, mempertahankan modular monolith atau bahkan menerima sedikit duplikasi sering kali lebih murah daripada menambah shared library atau memecah sistem menjadi service terpisah.

Prinsip ini sejalan dengan gagasan reuse less software: reuse bukan selalu gratis. Reuse yang dipaksakan sering menambah coupling, memperbesar blast radius saat ada bug atau celah keamanan, mempersulit upgrade, dan memperberat operasional. Artikel ini membahas cara mengambil keputusan teknis secara praktis: kapan tetap modular di dalam monolit, kapan shared library masuk akal, dan kapan service terpisah benar-benar layak.

Mengapa reuse bisa lebih mahal daripada yang terlihat

Secara intuitif, reuse tampak efisien: logika ditulis sekali, dipakai banyak tempat. Masalahnya, biaya sebenarnya jarang ada di baris kode yang berhasil dihemat. Biaya terbesar biasanya muncul di koordinasi perubahan, versi, kontrak antarkomponen, dan dampak perubahan ke banyak sistem sekaligus.

Contoh sederhana: tim memiliki tiga aplikasi yang butuh validasi alamat. Lalu dibuat shared library address-core. Awalnya terlihat rapi. Beberapa bulan kemudian, satu aplikasi butuh aturan baru untuk pasar tertentu, aplikasi kedua butuh format lama demi kompatibilitas, dan aplikasi ketiga hanya memakai 20% fungsi library tersebut. Sekarang tim menghadapi:

  • perdebatan API library yang cocok untuk semua konsumen,
  • upgrade versi yang tidak sinkron,
  • ketergantungan transitif yang ikut terbawa,
  • perubahan kecil yang harus diuji di banyak repositori.

Kalau logika itu tetap lokal di modul masing-masing, mungkin ada sedikit duplikasi, tetapi perubahan bisa dilakukan lebih cepat dan lebih aman. Ini bukan argumen anti-reuse total, melainkan pengingat bahwa reuse hanya menguntungkan bila variasi kebutuhan rendah dan batas abstraksinya benar-benar stabil.

Kapan mengurangi dependensi lebih murah daripada pecah service

Keputusan memecah service sering muncul karena alasan yang terdengar masuk akal: ingin pemisahan tanggung jawab, deployment independen, atau skalabilitas. Namun sebelum mengekstrak service, tanyakan: apakah masalah utamanya benar-benar akan selesai dengan jaringan, API, autentikasi antarservice, retry, tracing, dan operasional tambahan?

Tanda bahwa tetap di monolit modular lebih murah

  • Domain boundary belum stabil. Jika tim masih sering memindah tanggung jawab antarmodul, memecah service terlalu cepat hanya mengubah refactor internal menjadi migrasi antarservice.
  • Skala tim belum menuntut otonomi penuh. Pada tim kecil, overhead koordinasi, pipeline, secret, observability, dan incident response untuk banyak service sering lebih mahal daripada manfaatnya.
  • Kebutuhan throughput belum berbeda jauh. Jika semua modul tumbuh bersama dan masih bisa diskalakan sebagai satu unit, pemisahan service belum memberi keuntungan nyata.
  • Transaksi lintas proses akan memperumit konsistensi. Operasi yang sebelumnya atomik di database yang sama akan berubah menjadi saga, outbox, retry, dan kompensasi.
  • Masalah sebenarnya ada pada dependensi bersama, bukan struktur deploy. Sering kali bottleneck ada pada shared library gemuk, skema database bersama yang bocor ke banyak area, atau utilitas global yang sulit diubah.

Tanda bahwa shared library justru mulai merugikan

  • Library terus berubah demi kebutuhan satu konsumen tertentu. Ini tanda abstraksi tidak generik, melainkan akumulasi pengecualian.
  • Upgrade mahal dan tertunda. Jika setiap upgrade butuh sinkronisasi lintas tim dan regresi sering terjadi, biaya reuse sudah tinggi.
  • Blast radius besar. Satu bug atau celah keamanan pada library berdampak ke banyak aplikasi sekaligus.
  • Onboarding membingungkan. Engineer baru harus memahami lapisan utilitas internal yang tebal sebelum bisa mengubah fitur sederhana.
  • Abstraksi menyembunyikan perbedaan domain. Nama API terlihat umum, tetapi perilaku sebenarnya spesifik per produk.

Dalam kondisi seperti itu, mengurangi dependensi bisa berarti:

  • menghapus shared helper yang terlalu generik,
  • memindahkan logika kembali ke modul/domain yang memakainya,
  • menduplikasi sedikit kode yang stabil dan kecil,
  • menegaskan boundary modul di dalam monolit tanpa memecah deployment.

Matriks keputusan: monolit modular vs shared library vs service terpisah

Tabel berikut membantu memilih bentuk reuse yang paling murah untuk konteks tertentu.

PilihanKapan cocokKelebihanBiaya utamaRisiko umum
Monolit modularDomain masih berkembang, tim kecil/menengah, transaksi kuat dibutuhkan, operasional ingin sederhanaRefactor cepat, observability sederhana, deployment tunggal, konsistensi data lebih mudahButuh disiplin boundary internal, risiko modul saling bocor bila tidak dijagaMenjadi monolit berantakan jika semua kode bebas saling memanggil
Shared libraryLogika benar-benar stabil, sempit, dan dipakai banyak tempat dengan kebutuhan serupaMengurangi duplikasi pada area yang matang, distribusi lebih ringan daripada serviceVersioning, kompatibilitas, upgrade lintas repositori, blast radius keamananAbstraksi menjadi terlalu umum dan memaksa banyak kasus berbeda ke satu API
Service terpisahBoundary domain jelas, kebutuhan skala/isolasi nyata, lifecycle deployment berbeda, SLA spesifikIsolasi kegagalan lebih baik, ownership tim lebih jelas, deployment independenOperasional tinggi: jaringan, auth, observability, retry, kontrak API, data consistencyDistributed monolith: service banyak tetapi coupling tetap tinggi

Aturan praktisnya:

  • Mulai dari monolit modular bila masih mungkin.
  • Naik ke shared library hanya untuk logika yang sempit, matang, dan tidak sering berubah mengikuti variasi produk.
  • Pecah menjadi service hanya jika ada kebutuhan isolasi yang nyata, bukan sekadar keinginan “lebih rapi”.

Dampak arsitektur pada coupling, keamanan, upgrade, dan operasional

1. Coupling: lokal vs tersembunyi

Pada monolit modular, coupling biasanya lebih terlihat. Modul dapat dianalisis lewat import, package boundary, atau aturan arsitektur. Pada shared library dan service, coupling sering tampak lebih rapi di permukaan, tetapi sebenarnya lebih sulit diubah karena sudah menjadi kontrak lintas repositori atau lintas jaringan.

Coupling yang terlihat sering lebih murah daripada coupling yang tersembunyi. Jika modul A memanggil modul B di satu codebase, refactor masih relatif murah. Jika A bergantung pada library yang dipakai 12 aplikasi, atau pada service yang punya kontrak publik, perubahan jauh lebih mahal.

2. Blast radius keamanan

Shared library yang dipakai luas memperbesar dampak celah keamanan. Satu kerentanan dalam parser, validator, atau helper autentikasi bisa memaksa patch di banyak aplikasi. Service terpisah juga membawa permukaan serangan baru: endpoint jaringan, kredensial antarservice, policy akses, dan kemungkinan salah konfigurasi.

Monolit modular tidak otomatis lebih aman, tetapi permukaan operasionalnya lebih kecil. Jumlah secret, koneksi antarproses, dan policy jaringan biasanya lebih sedikit. Jika kontrol akses internal dan boundary modul rapi, audit keamanan bisa lebih mudah.

3. Biaya upgrade

Biaya upgrade sering diremehkan saat membuat shared library. Setiap konsumen harus:

  • menyesuaikan versi dependency,
  • memperbaiki perubahan API,
  • menjalankan regression test,
  • merilis ulang aplikasinya sendiri.

Jika library berubah cepat, organisasi bisa terjebak di banyak versi aktif sekaligus. Pada service terpisah, upgrade tidak dilakukan lewat package manager, tetapi lewat kontrak API dan kompatibilitas. Itu tetap mahal: versi endpoint, skema event, timeout, dan backward compatibility harus dikelola.

Pada monolit modular, perubahan internal sering bisa dilakukan dalam satu commit atau satu release train. Selama boundary modul dijaga, biaya perubahan biasanya lebih rendah.

4. Operasional dan observability

Service terpisah menambah kebutuhan nyata:

  • service discovery atau routing,
  • timeout dan retry policy,
  • distributed tracing,
  • log correlation,
  • health check,
  • rate limiting,
  • monitoring dependency antarservice.

Jika tim belum siap secara operasional, pemecahan service malah memindahkan kompleksitas dari kode ke infrastruktur. Masalah yang tadinya bug lokal berubah menjadi insiden lintas service yang sulit dilacak.

Catatan: banyak sistem tidak gagal karena logika bisnisnya terlalu sulit, tetapi karena dependency graph dan perilaku runtime-nya terlalu kompleks untuk diobservasi.

5. Onboarding dan maintainability

Engineer baru biasanya lebih cepat produktif di monolit modular yang boundary-nya jelas daripada di ekosistem multi-service dengan shared library internal yang banyak. Pada arsitektur yang terlalu terfragmentasi, orang harus memahami:

  • repositori mana yang relevan,
  • kontrak API dan event,
  • cara menjalankan dependensi lokal,
  • alur deployment beberapa komponen,
  • lapisan abstraksi internal yang dibangun bertahun-tahun.

Maintainability bukan sekadar “kode dipisah”, tetapi seberapa mudah sistem diubah dengan aman. Kadang jawaban terbaik adalah lebih sedikit komponen, bukan lebih banyak.

Implementasi praktis: jaga modularitas di dalam monolit

Jika Anda memutuskan belum perlu memecah service, keputusan itu harus dibarengi disiplin desain. Monolit modular yang baik bukan kumpulan folder acak, tetapi codebase dengan boundary eksplisit.

Struktur modul berdasarkan domain, bukan layer global

Hindari pola semua kode masuk ke folder generik seperti utils/, services/, atau common/ tanpa ownership jelas. Lebih aman jika struktur mengikuti domain:

src/
  billing/
    application/
    domain/
    infra/
    api/
  orders/
    application/
    domain/
    infra/
    api/
  identity/
    application/
    domain/
    infra/
    api/

Dengan struktur seperti ini, reuse internal tetap mungkin, tetapi dilakukan secara sadar. Bila orders butuh kemampuan dari billing, buat interface yang jelas di level aplikasi/domain, bukan impor bebas ke semua detail implementasi.

Bedakan shared kernel yang kecil dari dumping ground

Jika harus ada kode bersama, jaga agar sangat sempit: tipe dasar, utilitas murni, atau komponen yang benar-benar stabil. Jangan jadikan area shared sebagai tempat membuang semua kebutuhan lintas tim.

Tanda shared kernel masih sehat:

  • ukuran kecil,
  • jarang berubah,
  • tidak membawa dependency berat,
  • tidak memuat aturan bisnis spesifik satu domain.

Gunakan kontrak internal, bukan akses liar

Walau masih satu proses, perlakukan modul seperti punya kontrak. Misalnya, modul orders hanya memanggil facade atau service application dari billing, bukan langsung menyentuh tabel, repository, atau helper internal modul lain.

// Contoh pseudocode boundary internal
interface BillingPort {
  reserveCredit(customerId, amount): ReservationResult
}

class PlaceOrderHandler {
  constructor(private billing: BillingPort, private orders: OrderRepository) {}

  async execute(cmd) {
    const reservation = await this.billing.reserveCredit(cmd.customerId, cmd.total)
    if (!reservation.ok) throw new Error('credit denied')
    return this.orders.save(/* ... */)
  }
}

Pola ini membuat ekstraksi ke service di masa depan lebih murah karena kontraknya sudah ada, tetapi Anda belum membayar biaya jaringan sekarang.

Skenario realistis: tim kecil dan tim menengah

Tim kecil: 5-8 engineer, satu produk utama

Kondisi umum:

  • satu codebase backend,
  • satu database utama,
  • fitur masih sering berubah,
  • orang yang sama menangani fitur, bug, dan operasional.

Dalam kondisi ini, memecah service terlalu cepat hampir selalu menambah beban. Lebih masuk akal untuk:

  • memecah codebase menjadi modul domain,
  • membatasi dependency antar modul,
  • menghindari shared library internal kecuali sangat stabil,
  • mencatat area yang mungkin kelak layak diekstrak.

Contoh keputusan baik: validasi promo di dua modul sedikit berbeda, jadi tim sengaja membiarkan dua implementasi lokal. Duplikasi kecil diterima karena perubahan bisnis tiap modul juga berbeda. Ini lebih murah daripada memaksa satu promo-core yang penuh flag dan pengecualian.

Tim menengah: 15-30 engineer, beberapa alur bisnis kritikal

Pada fase ini, sebagian ekstraksi bisa masuk akal, tetapi pilih area yang benar-benar punya alasan kuat. Kandidat service yang lebih sehat biasanya memiliki:

  • boundary domain jelas,
  • aturan akses yang sensitif,
  • kebutuhan skalabilitas berbeda,
  • siklus rilis lebih independen.

Contoh: modul identity/auth sering layak dipisah lebih dulu jika banyak aplikasi internal bergantung padanya dan kebutuhan audit/keamanan tinggi. Sebaliknya, modul seperti perhitungan pajak lokal yang masih sering berubah bersama order flow sering lebih murah tetap di monolit sampai bentuk domainnya matang.

Tanda reuse berlebihan mulai merugikan

  • Setiap perubahan kecil butuh koordinasi dengan banyak tim atau banyak repositori.
  • Library bersama memiliki changelog panjang tetapi hanya sedikit konsumen yang benar-benar butuh seluruh fiturnya.
  • Engineer sering takut menyentuh modul shared karena risiko regresi luas.
  • Service dipisah, tetapi deploy tetap harus serempak karena kontraknya rapuh.
  • Masalah debugging meningkat karena alur request melewati terlalu banyak hop.
  • Waktu onboarding membengkak karena arsitektur lebih rumit daripada domain bisnisnya.
  • Incident review berulang menunjukkan kegagalan pada dependency umum berdampak ke banyak area sekaligus.

Jika beberapa gejala ini muncul, jangan langsung menambah lapisan baru. Sering kali solusi yang lebih murah adalah mengurangi reuse: pindahkan tanggung jawab kembali ke domain, persempit API shared, atau gabungkan kembali komponen yang terlalu dini dipisah.

Checklist evaluasi sebelum ekstraksi service

Sebelum memecah service, gunakan checklist berikut. Jika banyak jawaban masih “belum”, kemungkinan Anda lebih baik memperkuat modularitas internal dulu.

  1. Apakah boundary domain-nya stabil?
    Bisakah tim menjelaskan dengan jelas apa yang dimiliki service ini dan apa yang tidak?
  2. Apakah ada kebutuhan isolasi yang nyata?
    Misalnya kebutuhan skala, keamanan, compliance, atau ketersediaan yang berbeda.
  3. Apakah kontrak antarbagian sudah jelas di dalam monolit?
    Jika belum ada API internal yang rapi, ekstraksi ke jaringan biasanya prematur.
  4. Apakah tim siap mengelola operasional tambahan?
    Logging, tracing, auth antarservice, deployment, rollback, timeout, retry, dan alerting.
  5. Apakah model data lintas batas sudah dipahami?
    Bagaimana konsistensi dijaga jika transaksi tidak lagi satu database?
  6. Apakah biaya debugging bisa diterima?
    Bisakah tim melacak request lintas service dengan alat yang tersedia sekarang?
  7. Apakah ownership tim jelas?
    Service tanpa owner yang tegas sering menjadi bottleneck baru.
  8. Apakah kegagalan dependency ini sekarang menjadi sumber masalah utama?
    Jangan pecah service untuk masalah yang sebenarnya berasal dari desain modul yang buruk.
  9. Apakah ada strategi migrasi bertahap?
    Termasuk kontrak sementara, dual write bila perlu, dan rencana rollback.
  10. Apakah duplikasi kecil sebenarnya lebih murah?
    Jawaban ini sering tidak nyaman, tetapi kadang paling benar.

Pedoman keputusan yang bisa dipakai besok

Jika Anda membutuhkan aturan praktis yang sederhana:

  • Pilih monolit modular sebagai default saat domain masih bergerak dan tim belum besar.
  • Gunakan shared library hanya untuk hal yang kecil, stabil, dan benar-benar umum.
  • Pecah service hanya bila ada alasan isolasi yang konkret, bukan demi estetika arsitektur.
  • Terima duplikasi yang disengaja jika itu mengurangi coupling dan memperkecil blast radius.
  • Optimalkan biaya perubahan, bukan hanya jumlah baris kode yang dihemat.

Pada akhirnya, pertanyaan yang paling berguna bukan “bisakah ini direuse?” tetapi “siapa yang akan membayar biaya perubahan dari reuse ini dalam 6-12 bulan ke depan?” Jika jawabannya adalah banyak tim, banyak repositori, banyak deployment, dan banyak incident surface, maka mengurangi dependensi kemungkinan memang lebih murah daripada pecah service.