Pembukaan source code Microsoft Comic Chat memberi konteks yang menarik untuk pertanyaan yang sering muncul saat menghidupkan kembali aplikasi chat lama: apakah sistem sebaiknya dipertahankan sebagai satu codebase, diubah menjadi monolit modular, atau dipecah menjadi layanan terpisah? Untuk banyak proyek chat lama, jawaban praktisnya bukan langsung microservices. Pada tahap awal modernisasi, monolit modular atau modular monolith biasanya lebih masuk akal karena biaya operasi lebih rendah, latensi antarkomponen lebih kecil, dan debugging lebih sederhana.

Jika tujuan Anda adalah membuat aplikasi chat lama kembali berjalan, aman, mudah dipelihara, dan bisa berevolusi, keputusan arsitektur harus mengikuti bentuk beban kerja dan kapasitas tim, bukan tren. Konteks seperti yang dibahas dalam pengumuman open source Microsoft Comic Chat dapat menjadi titik masuk yang bagus untuk menilai ulang desain lama, tetapi fokus teknisnya tetap sama: pisahkan tanggung jawab yang jelas, perbaiki batas modul, dan pecah layanan hanya saat ada alasan operasional dan organisasi yang nyata.

Inti keputusan: untuk tim kecil-menengah yang memodernisasi aplikasi chat lama, mulai dari modular monolith hampir selalu lebih aman daripada langsung membangun banyak service. Pecah layanan ketika skala tim, pola deployment, isolasi kegagalan, atau kebutuhan performa benar-benar membutuhkannya.

Memahami istilah: monolit modular, modular monolith, dan microservices ringan

1. Monolit modular

Istilah ini sering dipakai longgar untuk menyebut satu aplikasi yang masih dideploy sebagai satu unit, tetapi source code-nya sudah dibagi ke modul-modul yang cukup jelas. Batas modul ada, namun penegakannya sering masih berbasis konvensi folder, package, atau namespace.

Cocok untuk fase transisi ketika codebase lama belum rapi, tetapi tim ingin mulai memisahkan domain seperti:

  • autentikasi dan sesi,
  • daftar kontak dan profil,
  • ruang chat dan membership,
  • pengiriman pesan,
  • notifikasi,
  • arsip atau histori pesan,
  • moderasi dan abuse handling.

2. Modular monolith

Modular monolith adalah bentuk yang lebih disiplin. Aplikasi tetap satu proses atau satu deployment utama, tetapi setiap modul memiliki batas yang tegas: API internal, kontrak data, dependensi yang dibatasi, dan idealnya kepemilikan domain yang jelas. Modul tidak saling mengakses tabel, objek, atau utilitas internal secara sembarangan.

Dalam praktik, ini sering menjadi target terbaik untuk proyek chat lama karena:

  • lebih mudah diuji dibanding monolit yang kusut,
  • lebih murah dioperasikan dibanding banyak service,
  • lebih mudah dipecah kemudian karena batas domain sudah terbentuk.

3. Microservices ringan atau layanan terpisah

Ini adalah arsitektur ketika beberapa domain dijalankan sebagai proses atau deployment berbeda, berkomunikasi lewat HTTP, gRPC, message broker, atau event bus. Disebut “ringan” jika jumlah service dibatasi dan pemecahan dilakukan hanya untuk area yang memang perlu, misalnya layanan presence atau media attachment.

Penting untuk dicatat: memecah aplikasi chat menjadi banyak service tidak otomatis membuat sistem lebih modern. Jika observability, deployment, ownership, dan kontrak data belum matang, hasilnya justru lebih rapuh daripada satu codebase yang modular.

Karakteristik aplikasi chat lama yang memengaruhi keputusan arsitektur

Aplikasi chat bukan CRUD biasa. Bahkan versi lama sering punya kombinasi kebutuhan berikut:

  • state yang berubah cepat, misalnya online presence, typing indicator, last seen, dan unread count,
  • latensi rendah untuk pengiriman pesan dan update UI,
  • urutan kejadian yang penting di level percakapan,
  • sinkronisasi real-time antara klien, server, cache, dan penyimpanan permanen,
  • kompatibilitas mundur dengan format data, protokol, atau perilaku lama,
  • moderasi dan keamanan yang sering ditambahkan belakangan pada produk lama.

Karena itu, pemecahan layanan yang terlalu dini sering memindahkan kompleksitas dari dalam proses ke jaringan. Masalah yang tadinya berupa pemanggilan fungsi berubah menjadi timeout, retry, idempotency, event ordering, partial failure, dan debugging lintas service.

Membandingkan pilihan arsitektur secara praktis

AspekMonolit modularModular monolithLayanan terpisah / microservices ringan
Kompleksitas timRendahSedangTinggi
Biaya operasionalPaling rendahRendah-sedangPaling tinggi
ObservabilityMudah dimulaiCukup mudahLebih sulit, perlu tracing dan korelasi log
DeploymentSatu unitSatu unit dengan disiplin modulBanyak pipeline dan koordinasi versi
Data consistencyMudah, sering bisa transaksi lokalMudah-menengahSulit, sering eventual consistency
LatensiRendahRendahLebih tinggi karena jaringan
MaintainabilityBaik jika disiplinSangat baik jika boundary dijagaBaik hanya jika tim dan tooling matang
Isolasi kegagalanTerbatasTerbatas-menengahLebih baik, jika desain benar
Skalabilitas per domainTerbatasTerbatas-menengahBaik untuk domain tertentu
Kecepatan debuggingCepatCepatLambat tanpa tracing yang baik

Kompleksitas tim

Jika tim Anda terdiri dari 3-10 engineer, memelihara banyak service biasanya terlalu mahal. Setiap service butuh CI/CD, monitoring, alerting, pengelolaan dependency, secret, dan dokumentasi kontrak. Tim kecil lebih diuntungkan oleh satu codebase yang punya boundary kuat.

Biaya operasional

Layanan terpisah berarti lebih banyak container, environment variable, endpoint, network policy, dashboard, dan insiden lintas komponen. Pada proyek kebangkitan aplikasi chat lama, biaya ini jarang sebanding di fase awal. Anda lebih sering butuh stabilitas dan pemahaman domain, bukan topologi distribusi yang rumit.

Observability

Pada monolit, satu request biasanya bisa dilacak lewat satu log stream dan satu profiler. Pada microservices, satu aksi sederhana seperti “kirim pesan” bisa melibatkan API gateway, auth service, message service, notification worker, presence service, dan storage service. Tanpa correlation ID, tracing terdistribusi, dan struktur log yang konsisten, insiden akan sulit dianalisis.

Deployment

Satu deployment memang berarti perubahan kecil tetap ikut merilis aplikasi yang sama, tetapi risikonya bisa dikendalikan dengan feature flag, migrasi schema yang aman, dan pengujian kontrak internal. Sebaliknya, banyak service memberi fleksibilitas deployment, namun menambah koordinasi kompatibilitas antarversi.

Data consistency

Fitur chat sering memerlukan konsistensi yang jelas di level percakapan: pesan tersimpan, indeks percakapan diperbarui, unread count berubah, notifikasi dibuat. Dalam monolit, banyak langkah ini bisa dilakukan dalam satu transaksi database atau satu alur sinkron plus queue lokal. Dalam layanan terpisah, Anda harus menerima bahwa sebagian besar alur akan menjadi eventually consistent.

Latensi

Pengguna chat merasakan latensi kecil. Setiap hop jaringan tambahan memperbesar peluang timeout dan jitter. Jika modul pengiriman pesan, validasi membership, dan persistence masih bisa hidup dalam satu proses, sering kali itu pilihan terbaik. Pisahkan hanya area yang memang independen, misalnya pemrosesan attachment atau push notification yang sifatnya asinkron.

Maintainability

Microservices hanya lebih mudah dipelihara jika batas domainnya benar, tim memiliki ownership jelas, dan perubahan antarservice jarang saling mengganggu. Jika tidak, Anda mendapat distribusi tanpa desentralisasi yang sehat: banyak repositori, tetapi coupling tetap tinggi.

Kapan satu codebase lebih masuk akal daripada memecah layanan

Untuk proyek chat lama, mempertahankan satu codebase lebih masuk akal bila beberapa kondisi berikut masih berlaku:

  • tim engineering kecil atau kapasitas DevOps terbatas,
  • domain belum benar-benar dipahami ulang setelah audit codebase lama,
  • masih banyak coupling data yang belum dipetakan,
  • produk belum punya beban trafik yang memaksa scaling per domain,
  • fokus utama masih stabilisasi, keamanan, dan modernisasi dasar,
  • insiden lebih sering berasal dari logika bisnis atau data lama, bukan bottleneck isolasi service.

Pada fase ini, target arsitektur yang sehat biasanya:

  1. tetap satu repositori atau satu codebase utama,
  2. pisahkan domain jadi modul yang tegas,
  3. standarkan kontrak internal,
  4. hindari akses database silang antar modul,
  5. jadikan queue dan event internal sebagai batas integrasi,
  6. baru pecah service saat satu modul benar-benar menuntut independensi operasional.

Contoh pembagian modul untuk modernisasi aplikasi chat lama

Berikut contoh struktur modular monolith yang realistis. Contoh ini tidak terikat framework tertentu, tetapi menunjukkan pemisahan domain yang bisa diuji dan diekstrak nanti.

src/
  modules/
    auth/
      api/
      application/
      domain/
      infrastructure/
    users/
      api/
      application/
      domain/
      infrastructure/
    conversations/
      api/
      application/
      domain/
      infrastructure/
    messaging/
      api/
      application/
      domain/
      infrastructure/
    presence/
      api/
      application/
      domain/
      infrastructure/
    notifications/
      api/
      application/
      domain/
      infrastructure/
    moderation/
      api/
      application/
      domain/
      infrastructure/
  shared/
    logging/
    events/
    db/
    cache/

Setiap modul idealnya memiliki:

  • API internal yang eksplisit, bukan akses langsung ke class atau tabel modul lain,
  • application layer untuk orchestration use case,
  • domain layer untuk aturan bisnis,
  • infrastructure layer untuk DB, cache, broker, dan adapter eksternal.

Contoh kontrak internal antarmodul

Alih-alih modul notifications membaca tabel pesan langsung, modul messaging dapat menerbitkan event internal ketika pesan berhasil dikirim.

// pseudo-code
interface DomainEvent {
  type: string;
  occurredAt: Date;
  payload: object;
}

class MessageSent implements DomainEvent {
  type = "message.sent";
  occurredAt = new Date();

  constructor(public payload: {
    conversationId: string;
    messageId: string;
    senderId: string;
    recipientIds: string[];
  }) {}
}

Pola ini membantu karena:

  • ketergantungan antar modul lebih longgar,
  • alur asinkron seperti notifikasi lebih mudah dipindahkan ke worker,
  • event yang sama kelak bisa menjadi titik integrasi jika modul dipecah menjadi service.

Kapan mulai migrasi ke layanan terpisah

Jangan memecah layanan karena “aplikasi chat harus real-time maka harus microservices”. Itu asumsi yang lemah. Gunakan sinyal teknis yang lebih konkret.

Sinyal yang valid untuk memecah modul menjadi service

  • Skala beban sangat berbeda: misalnya presence atau WebSocket gateway butuh scale-out jauh lebih agresif dibanding admin panel atau profil pengguna.
  • Kebutuhan runtime berbeda: misalnya attachment processing perlu worker terisolasi dengan dependency sistem yang tidak cocok dengan API utama.
  • Failure isolation penting: gangguan pada notifikasi tidak boleh mengganggu pengiriman pesan inti.
  • Siklus rilis berbeda: satu domain berubah cepat dan sering dirilis tanpa ingin menunggu aplikasi utama.
  • Batas data sudah jelas: modul punya ownership data yang relatif mandiri dan integrasi lintas modul sudah melalui API/event, bukan query langsung.
  • Observability dan operasi sudah siap: ada distributed tracing, correlation ID, dashboard service-level, dan on-call process yang masuk akal.

Sinyal yang belum cukup kuat

  • codebase terasa besar, tetapi boundary modul belum dibuat,
  • satu query lambat lalu langsung menyalahkan monolit,
  • ingin terlihat modern,
  • tim belum punya pengalaman menjalankan sistem terdistribusi,
  • masalah utama sebenarnya pada schema database, cache, atau N+1 query.

Strategi bertahap yang aman untuk tim kecil-menengah

Tahap 1: stabilisasi dan audit domain

Sebelum menyentuh topologi service, petakan domain utama aplikasi chat lama:

  • bagaimana autentikasi dan sesi bekerja,
  • di mana pesan disimpan,
  • bagaimana unread count dihitung,
  • bagaimana presence dikelola,
  • komponen mana yang sinkron dan mana yang bisa asinkron.

Pada tahap ini, tambahkan juga pengujian karakterisasi untuk perilaku lama yang masih perlu dipertahankan.

Tahap 2: bentuk modular monolith

Pisahkan codebase berdasarkan domain, bukan layer teknis umum semata. Banyak tim membuat folder controllers, services, dan repositories global, lalu semua domain bercampur lagi di dalamnya. Untuk aplikasi chat, lebih baik setiap domain punya lapisan sendiri.

Tahap 3: perkenalkan event internal dan queue untuk pekerjaan asinkron

Beberapa proses tidak perlu sinkron, misalnya:

  • push notification,
  • pengindeksan pencarian,
  • fan-out ke analitik,
  • pemindaian konten moderasi tertentu,
  • thumbnail atau attachment processing.

Langkah ini memberi dua manfaat: mengurangi coupling sinkron, dan mempersiapkan jalur migrasi ke layanan terpisah tanpa memaksa distribusi terlalu cepat.

Tahap 4: ekstrak service yang paling jelas batasnya

Jika memang perlu microservices ringan, mulai dari modul yang:

  • punya beban kerja berbeda,
  • kontrak integrasinya sudah sederhana,
  • tidak memerlukan transaksi lintas domain yang ketat.

Contoh kandidat yang sering lebih aman diekstrak lebih dulu:

  • attachment/media processing,
  • push notification delivery,
  • search indexing,
  • presence gateway jika beban koneksi real-time sangat tinggi.

Yang biasanya jangan langsung diekstrak pertama kali adalah domain inti pengiriman pesan jika model data dan invariant bisnisnya belum benar-benar bersih.

Data consistency pada sistem chat: jangan diremehkan

Salah satu alasan kuat memilih modular monolith untuk proyek chat lama adalah kemudahan menjaga konsistensi. Misalnya use case “kirim pesan” sering membutuhkan langkah seperti:

  1. validasi pengirim adalah anggota percakapan,
  2. simpan pesan,
  3. perbarui metadata percakapan,
  4. buat status unread untuk penerima,
  5. jadwalkan notifikasi,
  6. siarkan update ke koneksi real-time.

Dalam satu aplikasi, Anda bisa membedakan mana yang wajib sinkron dan mana yang dapat ditunda. Contohnya, penyimpanan pesan dan metadata percakapan biasanya satu jalur transaksi logis, sedangkan notifikasi push bisa ditunda.

Dalam arsitektur layanan terpisah, Anda harus menerima bahwa sebagian state akan tertinggal sementara. Itu tidak salah, tetapi harus didesain dengan sadar. Anda perlu:

  • event yang idempotent,
  • retry policy yang aman,
  • dead-letter handling,
  • mekanisme deduplikasi,
  • strategi rekonsiliasi jika event hilang atau tertunda.

Observability minimum sebelum memecah layanan

Banyak migrasi gagal bukan karena desain domain, tetapi karena tim kehilangan visibilitas setelah sistem tersebar. Sebelum memecah service, pastikan minimal ada:

  • correlation ID yang dibawa dari request masuk sampai worker asinkron,
  • structured logging dengan field konsisten seperti conversation_id, user_id, message_id,
  • metrics dasar: error rate, latency, queue depth, retry count, active connections,
  • trace untuk alur penting seperti login, join room, send message, sync history,
  • health check dan readiness check yang realistis.

Jika semua ini belum ada, menambah service hanya akan memperbesar area gelap dalam debugging.

Anti-pattern umum saat memodernisasi aplikasi chat lama

1. Distributed monolith

Service sudah dipisah, tetapi masih saling bergantung kuat, harus dirilis bersama, dan sering memanggil database satu sama lain. Anda mendapat overhead jaringan tanpa manfaat isolasi yang nyata.

2. Shared database antarservice

Pada fase transisi mungkin sulit dihindari, tetapi menjadikannya permanen biasanya memperburuk coupling. Jika service A dan B bebas mengubah tabel yang sama, boundary domain menjadi ilusi.

3. Memecah berdasarkan layer teknis, bukan domain

Contoh buruk: auth-service, database-service, websocket-service, business-service tanpa batas tanggung jawab yang jelas. Hasilnya justru percakapan bisnis tersebar ke banyak tempat.

4. Menjadikan semua hal sinkron

Notifikasi, analitik, indexing, dan moderasi tertentu tidak harus berada dalam jalur request utama. Jalur sinkron yang terlalu panjang akan memperbesar latensi dan memperburuk kegagalan berantai.

5. Mengabaikan urutan event

Dalam chat, urutan pesan dan status sangat sensitif. Jika Anda memecah service, definisikan dengan jelas apa yang harus terurut secara ketat dan apa yang boleh eventual.

6. Langsung mengganti semuanya sekaligus

Rewrite penuh sambil mengubah arsitektur dan protokol sekaligus adalah risiko tinggi. Lebih aman memisahkan concern satu per satu sambil menjaga kompatibilitas perilaku inti.

Contoh aturan keputusan sederhana

Anda bisa memakai aturan praktis berikut saat menilai modul dalam aplikasi chat lama:

  • Tetap di monolit modular jika masalah utamanya adalah code health, testability, dan refactor boundary.
  • Naik ke modular monolith jika Anda butuh disiplin domain yang kuat, tetapi operasi masih ingin sederhana.
  • Ekstrak jadi service jika modul punya pola skala, failure isolation, dan lifecycle deployment yang jelas berbeda.

Dalam banyak kasus, kombinasi paling sehat adalah: inti chat tetap modular monolith, fitur periferal dibuat service bila perlu.

Rekomendasi akhir untuk tim kecil-menengah

Jika Anda sedang menghidupkan kembali proyek seperti aplikasi chat lama dengan warisan codebase yang belum rapi, jangan mulai dari microservices. Mulailah dari modular monolith:

  1. rapikan domain inti: auth, conversations, messaging, presence, notifications, moderation,
  2. tegakkan kontrak internal dan larang akses data sembarangan antar modul,
  3. gunakan event internal dan queue untuk pekerjaan asinkron,
  4. bangun observability dasar lebih dulu,
  5. ekstrak hanya modul yang benar-benar butuh independensi operasional.

Untuk proyek chat lama, pendekatan ini biasanya memberi rasio manfaat terhadap kompleksitas yang paling baik. Anda tetap bisa menuju layanan terpisah nanti, tetapi dengan dasar yang lebih aman: boundary domain sudah nyata, coupling berkurang, dan kebutuhan operasional tiap modul sudah terlihat dari data, bukan asumsi.

Pada akhirnya, keputusan antara monolit modular vs layanan terpisah untuk proyek chat lama bukan soal memilih arsitektur yang paling trendi. Ini soal memilih bentuk kompleksitas yang bisa ditanggung tim Anda sambil menjaga latensi, konsistensi data, dan kemudahan pemeliharaan. Untuk mayoritas tim kecil-menengah, satu codebase yang modular dan disiplin adalah langkah modernisasi yang paling rasional.