Dalam proyek CodeIgniter 4, keputusan untuk tetap memakai monolith, beralih ke modular monolith, atau memecah aplikasi menjadi service terpisah sebaiknya tidak didasarkan pada tren arsitektur. Jawaban singkatnya: monolith masih tepat untuk banyak aplikasi, modular monolith cocok saat basis kode mulai padat tetapi operasional ingin tetap sederhana, dan service terpisah layak ketika ada kebutuhan isolasi skalabilitas, batas domain yang jelas, atau kebutuhan rilis independen yang benar-benar nyata.

Masalahnya bukan apakah microservices terdengar lebih modern, tetapi apakah tim Anda siap menanggung biaya tambahan: deployment lebih rumit, observability lebih wajib, debugging lintas proses lebih sulit, kontrak API harus dijaga, dan kegagalan jaringan menjadi bagian normal dari sistem. Di sisi lain, monolith yang tidak disiplin juga bisa berubah menjadi kode besar yang sulit dirawat. Karena itu, keputusan terbaik biasanya bukan ekstrem, melainkan evolusi bertahap.

Memahami tiga pilihan arsitektur di CodeIgniter 4

1. Monolith

Semua fitur berjalan dalam satu aplikasi CodeIgniter 4, satu deployment unit, dan biasanya satu basis data utama. Modul seperti auth, katalog, checkout, dan notifikasi berada dalam repository dan proses runtime yang sama.

  • Kelebihan: sederhana untuk dikembangkan, diuji, dan di-deploy.
  • Kekurangan: bila tanpa batas internal yang jelas, coupling meningkat dan perubahan kecil bisa memengaruhi area lain.

2. Modular monolith

Aplikasi tetap satu deployment, tetapi struktur kode dipisah berdasarkan domain atau modul. Misalnya Auth, Catalog, Checkout, dan Notification punya controller, service, repository, DTO, dan aturan bisnis masing-masing.

  • Kelebihan: maintainability meningkat tanpa menambah kompleksitas operasional setara distributed system.
  • Kekurangan: disiplin arsitektur harus dijaga. Jika tidak, modul hanya formalitas folder.

3. Service terpisah

Domain tertentu diekstrak menjadi aplikasi tersendiri, misalnya service notifikasi atau auth. Masing-masing punya lifecycle deployment, konfigurasi, logging, dan kemungkinan basis data sendiri.

  • Kelebihan: isolasi skalabilitas, rilis independen, kepemilikan tim lebih jelas.
  • Kekurangan: network call, retry, timeout, observability, versioning API, dan sinkronisasi data menjadi tanggung jawab baru.

Kapan monolith di CodeIgniter 4 masih cukup?

CodeIgniter 4 sebagai monolith masih sangat masuk akal jika sebagian besar kondisi berikut terpenuhi:

  • Tim kecil sampai menengah, dan belum ada pembagian ownership domain yang kuat.
  • Deployment masih bisa dilakukan sebagai satu paket tanpa bottleneck besar.
  • Performa masalah utama masih ada di query database, caching, atau desain kode, bukan karena arsitektur monolith itu sendiri.
  • Fitur-fitur saling berbagi transaksi dan data secara erat, misalnya checkout perlu akses langsung ke cart, promo, stok, dan pembayaran.
  • Observability masih sederhana: log aplikasi, error tracking, dan metrik dasar sudah cukup.
  • Kebutuhan skalabilitas masih seragam, belum ada satu domain yang butuh autoscaling sangat berbeda dari domain lain.

Sinyal praktis bahwa monolith masih sehat

  • Waktu build dan deploy masih dapat diterima oleh tim.
  • Insiden produksi lebih sering disebabkan bug bisnis atau query lambat, bukan dependency antarbagian yang tak terkendali.
  • Pengujian integrasi masih bisa dijalankan tanpa setup yang terlalu mahal.
  • Perubahan pada modul katalog tidak sering merusak auth atau checkout.

Jika kondisi di atas masih berlaku, memecah service terlalu cepat sering hanya memindahkan masalah dari kode yang semestinya dirapikan menjadi operasional yang jauh lebih sulit.

Kapan modular monolith lebih tepat daripada langsung memecah service?

Modular monolith adalah langkah tengah yang sering paling rasional. Anda mulai memberi batas domain yang tegas tanpa menanggung biaya distributed system.

Sinyal bahwa modularisasi internal diperlukan

  • Folder aplikasi mulai padat dan sulit dipahami oleh anggota tim baru.
  • Logika bisnis bercampur antara controller, model, helper, dan library tanpa pemisahan yang konsisten.
  • Perubahan fitur di satu area sering melibatkan file dari banyak area lain karena dependency tidak terkendali.
  • Anda sudah bisa mengidentifikasi domain seperti auth, katalog, checkout, notifikasi, tetapi belum ada kebutuhan kuat untuk deployment terpisah.

Contoh pembagian modul

app/
  Modules/
    Auth/
      Controllers/
      Services/
      Repositories/
      Entities/
    Catalog/
      Controllers/
      Services/
      Repositories/
      Entities/
    Checkout/
      Controllers/
      Services/
      Repositories/
      DTO/
    Notification/
      Services/
      Channels/
      Templates/

Struktur ini bukan fitur ajaib; manfaatnya muncul jika Anda juga menetapkan aturan:

  • Controller tipis, logika bisnis di service/domain layer.
  • Akses data lewat repository atau boundary yang konsisten.
  • Modul checkout tidak langsung mengubah tabel auth tanpa kontrak yang jelas.
  • Notifikasi dipanggil melalui interface, bukan tersebar sebagai pemanggilan vendor API di banyak tempat.

Kenapa pendekatan ini sering berhasil?

Karena mayoritas masalah maintainability pada monolith bukan berasal dari fakta bahwa aplikasinya satu proses, melainkan dari coupling yang tidak dijaga. Modular monolith memperbaiki batas internal lebih dulu. Jika nanti satu modul memang perlu diekstrak, Anda sudah punya pemisahan domain yang lebih siap.

Kapan service terpisah benar-benar layak?

Ekstraksi service sebaiknya didorong oleh kebutuhan nyata, bukan asumsi masa depan. Beberapa indikator yang cukup kuat:

1. Kebutuhan skalabilitas sangat berbeda

Contoh klasik adalah notifikasi. Trafiknya bisa melonjak karena email, webhook, atau push, tetapi tidak berarti modul checkout juga harus di-scale dengan pola yang sama. Jika notifikasi punya beban asynchronous dan throughput berbeda, memisahkannya bisa masuk akal.

2. Dependency eksternal membuat domain lebih mandiri

Jika modul notifikasi berinteraksi dengan banyak provider, retry policy, fallback channel, dan rate limiting, domain itu mulai punya kompleksitas sendiri. Menjaganya sebagai service terpisah bisa mengurangi dampak perubahan ke inti aplikasi.

3. Siklus rilis independen benar-benar dibutuhkan

Misalnya tim auth perlu melakukan perubahan keamanan dan rollout lebih sering, sementara domain katalog relatif stabil. Jika pelepasan versi bersama menjadi hambatan nyata, service terpisah bisa membantu.

4. Kepemilikan domain dan tim sudah jelas

Arsitektur service tanpa ownership tim yang matang sering gagal. Jika belum ada tim yang benar-benar bertanggung jawab terhadap operasional, API contract, incident response, dan backlog domain tersebut, memisah service hanya menambah titik gagal.

5. Kebutuhan isolasi kegagalan

Service notifikasi yang gagal seharusnya tidak memblokir checkout inti. Jika kegagalan satu domain perlu diisolasi secara operasional, service terpisah atau setidaknya asynchronous boundary menjadi semakin relevan.

Contoh domain: mana yang biasanya lebih cocok dipisah?

  • Auth: cocok dipisah jika dipakai lintas banyak aplikasi, butuh SSO, token service terpusat, atau aturan keamanan khusus. Jika hanya untuk satu aplikasi, sering kali tetap lebih efisien di monolith/modular monolith.
  • Katalog: tetap di monolith jika hanya mendukung satu aplikasi. Layak dipisah jika melayani banyak kanal, punya pipeline indexing/search sendiri, atau beban baca sangat tinggi dan berbeda jauh dari modul lain.
  • Checkout: biasanya paling sensitif dan kaya transaksi. Sering kali lebih aman tetap dekat dengan domain inti sampai batas transaksi, konsistensi data, dan idempotency benar-benar tertangani.
  • Notifikasi: salah satu kandidat terbaik untuk service terpisah karena sifatnya asynchronous, provider-heavy, dan kebutuhan skalanya bisa berbeda.

Trade-off teknis yang sering diremehkan

Biaya operasional

Monolith membutuhkan satu pipeline utama, satu aplikasi untuk dimonitor, dan biasanya satu pola deployment. Service terpisah berarti lebih banyak container atau proses, lebih banyak environment variable, lebih banyak health check, dan lebih banyak runbook insiden.

Kompleksitas deployment

Pada monolith, satu release biasanya cukup. Pada arsitektur service, Anda harus memikirkan kompatibilitas versi API, deployment berurutan, rollback parsial, dan migrasi skema yang mungkin tidak lagi sinkron antarservice.

Observability

Di monolith, stack trace biasanya cukup langsung. Di service terpisah, Anda butuh correlation ID, centralized logging, distributed tracing atau setidaknya request flow yang konsisten, metrik per service, dan alert yang lebih presisi.

Konsistensi data

Memecah service sering berarti mengurangi transaksi ACID lintas domain. Anda harus siap dengan eventual consistency, outbox pattern, retry, idempotency, dan penanganan data yang terlambat sinkron.

Debugging

Bug di monolith biasanya bersifat lokal. Bug di service terpisah bisa muncul karena timeout, retry ganda, payload versi lama, antrian tertunda, atau data cache yang belum sinkron. Kompleksitas ini nyata, bukan teoritis.

Matriks keputusan: monolith vs modular monolith vs service terpisah

Kriteria Monolith Modular Monolith Service Terpisah
Ukuran tim Kecil-menengah Kecil-menengah yang mulai bertumbuh Menengah-besar dengan ownership domain jelas
Kompleksitas deployment Rendah Rendah-menengah Tinggi
Observability yang dibutuhkan Dasar Dasar-menengah Menengah-tinggi
Isolasi skalabilitas Terbatas Terbatas, tetapi kode lebih siap Kuat
Konsistensi data lintas domain Mudah Mudah Lebih sulit
Rilis independen per domain Tidak Tidak penuh Ya
Cocok untuk auth internal tunggal Ya Ya Tergantung kebutuhan lintas aplikasi
Cocok untuk notifikasi multi-provider Mungkin Baik sebagai langkah awal Sering paling masuk akal

Pola evolusi yang aman di proyek CodeIgniter 4

Pendekatan yang lebih aman biasanya bertahap:

  1. Rapikan monolith lebih dulu. Pisahkan controller, service, repository, validasi, dan DTO bila perlu.
  2. Tetapkan boundary modul. Domain seperti auth, katalog, checkout, dan notifikasi harus punya kontrak internal yang jelas.
  3. Kurangi panggilan silang langsung. Hindari modul saling menyentuh detail database satu sama lain.
  4. Tambahkan asynchronous boundary bila cocok. Contohnya notifikasi dipicu melalui job atau event internal, bukan diproses sinkron di alur checkout.
  5. Observasi bottleneck nyata. Ukur area yang sering deploy, sering gagal, atau butuh scaling berbeda.
  6. Ekstrak satu service yang paling jelas kasusnya. Biasanya bukan checkout, melainkan notifikasi atau auth lintas aplikasi.

Contoh boundary internal sebelum ekstraksi

<?php

namespace App\Modules\Checkout\Services;

use App\Modules\Notification\Contracts\NotifierInterface;

class CheckoutService
{
    public function __construct(private NotifierInterface $notifier)
    {
    }

    public function placeOrder(array $orderData): string
    {
        // simpan order, validasi stok, hitung total, dsb.
        $orderId = 'ORD-2024-0001';

        $this->notifier->sendOrderCreated($orderId);

        return $orderId;
    }
}

Jika notifikasi nantinya dipindah menjadi service terpisah, implementasi NotifierInterface dapat diganti tanpa mengubah terlalu banyak logika checkout. Ini contoh kecil kenapa modularisasi internal membantu migrasi.

Anti-pattern umum

1. Memecah service berdasarkan tabel, bukan domain

Jika pemisahan hanya mengikuti tabel database atau CRUD sederhana, hasilnya sering bukan service yang mandiri, tetapi API wrapper tipis yang saling memanggil terus-menerus. Latency naik, manfaat domain separation tidak terasa.

2. Distributed monolith

Ini terjadi saat aplikasi sudah dipecah menjadi banyak service, tetapi deployment harus selalu bersama, perubahan kecil memengaruhi semuanya, dan hampir setiap request membutuhkan panggilan ke banyak service lain. Anda mendapat kompleksitas microservices tanpa independensi yang dijanjikan.

3. Shared database lintas service tanpa aturan

Jika dua service menulis tabel yang sama secara bebas, batas domain menjadi palsu. Coupling tetap ada, hanya sekarang lebih sulit dideteksi karena tersembunyi di jaringan dan database.

4. Ekstraksi checkout terlalu dini

Checkout biasanya penuh aturan bisnis, transaksi, pembayaran, stok, promo, dan audit. Memisahkannya sebelum idempotency, compensating flow, dan konsistensi data siap justru meningkatkan risiko bug finansial.

5. Mengabaikan observability

Service terpisah tanpa trace ID, log terpusat, timeout, retry policy, dan metric dasar akan membuat insiden produksi sangat sulit diurai.

Risiko migrasi terlalu dini

  • Velocity turun: tim sibuk mengurus contract, gateway, auth antarservice, dan pipeline baru alih-alih mengirim fitur.
  • Biaya naik: infrastruktur, monitoring, dan debugging bertambah.
  • Bug baru: duplikasi request, timeout, event hilang, dan data tidak sinkron.
  • Ownership kabur: service baru ada, tetapi tidak ada tim yang benar-benar menjaganya.
  • Optimasi salah sasaran: masalah asli mungkin ada di query N+1, indexing buruk, atau cache yang belum diterapkan.

Catatan praktis: jika bottleneck utama masih bisa diselesaikan dengan profiling query, caching, queue, atau refactor boundary internal, itu biasanya lebih murah daripada memecah service.

Checklist evaluasi sebelum memecah aplikasi

  1. Apakah domain yang ingin dipisah memiliki batas bisnis yang jelas?
  2. Apakah service itu perlu skala yang berbeda dari aplikasi utama?
  3. Apakah tim benar-benar butuh deployment independen?
  4. Apakah ada owner domain yang akan menangani operasional dan incident response?
  5. Apakah kontrak API, validasi payload, timeout, dan retry sudah dirancang?
  6. Apakah Anda siap dengan logging terpusat, correlation ID, dan metric per service?
  7. Apakah konsistensi data antar domain bisa diterima jika menjadi eventual consistency?
  8. Apakah use case bisa diuji end-to-end tanpa biaya yang berlebihan?
  9. Apakah bottleneck yang ada sudah dibuktikan dengan data, bukan dugaan?
  10. Apakah modular monolith sudah dicoba lebih dulu sebagai langkah transisi?

Rekomendasi praktis per modul

Auth

Tetap di monolith atau modular monolith jika hanya dipakai satu aplikasi. Pisahkan jika auth menjadi platform bersama, butuh SSO, atau dipakai banyak aplikasi dengan lifecycle sendiri.

Katalog

Pertahankan di modular monolith jika domain ini masih erat dengan aplikasi utama. Pertimbangkan service terpisah bila ada kebutuhan search/indexing yang berat, banyak kanal konsumsi data, atau throughput baca jauh lebih tinggi.

Checkout

Utamakan kestabilan dan konsistensi. Biasanya lebih aman tetap di monolith/modular monolith lebih lama. Pisahkan hanya jika domain dan operasionalnya benar-benar matang.

Notifikasi

Sering menjadi kandidat pertama untuk diekstrak, terutama bila sudah asynchronous, melibatkan banyak provider, dan perubahan domain ini tidak seharusnya menghambat rilis aplikasi inti.

Kesimpulan

Dalam CodeIgniter 4: kapan pisah service dan kapan tetap monolith, keputusan yang sehat biasanya dimulai dari pertanyaan operasional dan batas domain, bukan preferensi gaya arsitektur. Monolith cocok saat tim ingin cepat, sederhana, dan domain masih saling terkait erat. Modular monolith cocok saat maintainability mulai menurun tetapi kompleksitas operasional belum layak dinaikkan. Service terpisah layak ketika ada alasan konkret: skala berbeda, rilis independen, dependency eksternal dominan, ownership tim jelas, dan observability siap.

Jika ragu, pilih langkah yang paling reversible: rapikan monolith, buat boundary modular, ukur bottleneck nyata, lalu ekstrak hanya domain yang memang terbukti mendapat manfaat. Untuk banyak proyek CodeIgniter 4, itu menghasilkan sistem yang lebih stabil dan lebih mudah dirawat dalam jangka panjang.