Memilih arsitektur tool replay session untuk codebase besar bukan soal menambah banyak komponen sejak awal, tetapi soal menempatkan bottleneck di tempat yang bisa dikendalikan. Dalam konteks seperti Mindwalk, yang memvisualisasikan peta kode 3D dan memutar ulang sesi coding berdasarkan event, keputusan arsitektur akan langsung memengaruhi latensi interaksi, penggunaan CPU/GPU/RAM, biaya operasional, serta kemudahan debugging.

Jawaban singkatnya: arsitektur sederhana biasanya cukup jika pemakai masih sedikit, replay dilakukan dari data lokal, dan graph kode dapat dimuat di satu proses tanpa menekan resource secara berlebihan. Namun ketika ukuran codebase membesar, sesi bertambah panjang, pengguna mulai kolaboratif, atau rendering dan indexing menjadi mahal, pemisahan service, pipeline async, dan penyimpanan graph yang lebih persisten mulai masuk akal. Kuncinya bukan “microservice vs monolith”, melainkan memisahkan jalur kerja yang memang punya karakter resource dan failure mode berbeda.

Mental model: apa yang sebenarnya dikerjakan tool replay session

Sebelum memilih arsitektur, pecah sistem menjadi empat aliran utama:

  1. Ingestion event: menangkap event editor, git, file system, cursor, selection, atau perubahan struktur kode.
  2. Pemrosesan dan normalisasi: mengurutkan event, menghapus noise, memperkaya dengan metadata, dan memetakan event ke node/edge pada graph kode.
  3. Penyimpanan state: menyimpan graph kode, timeline event, snapshot, dan indeks untuk replay.
  4. Rendering dan playback: menghasilkan visualisasi 3D, sinkronisasi timeline, kamera, highlight node, dan scrub replay.

Pada codebase kecil, keempat bagian ini sering bisa berjalan dalam satu proses lokal. Pada codebase besar, masing-masing punya profil beban berbeda:

  • Ingestion sensitif terhadap latency spike.
  • Pemrosesan graph sensitif terhadap CPU dan memori.
  • Penyimpanan sensitif terhadap ukuran data, query, dan consistency.
  • Rendering sensitif terhadap GPU, bandwidth, dan frame pacing.

Karena itu, arsitektur sebaiknya dipilih berdasarkan jalur kritis yang ingin diprioritaskan: respons interaktif lokal, throughput event, kolaborasi multi-user, atau efisiensi operasional.

Monolith lokal vs service terpisah

Kapan monolith lokal sudah cukup

Untuk banyak tim kecil, pendekatan paling masuk akal adalah monolith lokal: ingestion event, builder graph, penyimpanan ringan, dan renderer berjalan dalam satu aplikasi. Ini cocok bila:

  • Replay dipakai terutama oleh satu developer atau tim kecil.
  • Data sesi berasal dari mesin lokal dan tidak perlu sinkronisasi real-time lintas pengguna.
  • Ukuran graph masih dapat dimuat di RAM tanpa swapping.
  • Pipeline indexing dan rendering belum membutuhkan scaling terpisah.
  • Prioritas utama adalah kesederhanaan deployment dan kecepatan iterasi fitur.

Keunggulan monolith lokal:

  • Latensi rendah karena tidak ada hop jaringan antar komponen utama.
  • Biaya operasional rendah karena tidak perlu cluster, queue broker, atau database terpisah sejak awal.
  • Observability lebih sederhana: log dan stack trace lebih mudah ditelusuri dalam satu proses.
  • Maintainability awal lebih baik karena boundary masih berupa modul, belum kontrak jaringan.

Kekurangannya:

  • Satu crash bisa menjatuhkan ingestion, playback, dan UI sekaligus.
  • CPU-heavy task seperti rebuild graph dapat mengganggu frame rate rendering.
  • Sulit membedakan bottleneck I/O, CPU, dan GPU tanpa pemisahan pipeline yang jelas.
  • Ketika mulai butuh fitur kolaboratif atau pemrosesan batch, monolith cenderung tumbuh tidak terkendali.

Prinsip praktis: jika mayoritas masalah Anda masih seputar akurasi replay, pemetaan event ke graph, dan UX playback, jangan buru-buru memecah service. Pecahkan dulu di level modul dan profil performa.

Kapan service terpisah mulai diperlukan

Service terpisah relevan ketika ada komponen dengan pola skala dan failure mode berbeda. Contoh yang umum:

  • Ingestion service menerima event secara kontinu dan harus ringan.
  • Processing/indexing worker mengubah event mentah menjadi snapshot graph atau metadata replay.
  • Graph/query service melayani permintaan node, edge, neighborhood, atau timeline.
  • Rendering/streaming service untuk kasus di mana render lokal tidak realistis.

Pemisahan service masuk akal jika:

  • Event datang dari banyak sumber atau banyak sesi paralel.
  • Pemrosesan graph mahal dan tidak boleh memblokir jalur ingest.
  • Anda butuh retry, backpressure, dan isolasi kegagalan.
  • Data mulai dipakai lintas mesin atau lintas tim.
  • Perlu kontrol resource terpisah untuk CPU worker, memori graph, dan GPU render.

Trade-off utamanya:

  • Latensi: lebih tinggi karena serialisasi dan jaringan.
  • Biaya operasional: naik karena perlu deployment, service discovery, monitoring, dan storage tambahan.
  • Observability: secara teori lebih baik karena tiap komponen jelas, tetapi praktiknya lebih kompleks karena butuh tracing lintas service.
  • Maintainability: bisa lebih baik bila domain benar-benar terpisah, tapi bisa lebih buruk jika service dipisah terlalu dini.

Pola transisi yang aman

Sering kali pilihan terbaik bukan langsung “semua lokal” atau “semua terdistribusi”, melainkan modular monolith dengan satu atau dua komponen eksternal. Contoh evolusi yang sehat:

  1. Mulai dari aplikasi lokal dengan modul: ingest, graph, storage, render.
  2. Pisahkan hanya worker pemrosesan event jika mulai berat.
  3. Pisahkan graph/query service jika akses graph perlu dibagi ke banyak client atau proses.
  4. Pertimbangkan render server-side hanya jika constraint hardware atau akses jarak jauh memang dominan.

Pendekatan ini menjaga kompleksitas tetap proporsional terhadap kebutuhan nyata.

Pemrosesan event sinkron vs async

Sinkron: sederhana dan deterministik

Pemrosesan event sinkron berarti event masuk, diproses, lalu state replay atau graph diperbarui dalam jalur eksekusi yang sama. Untuk tool replay session, ini menarik karena:

  • Urutan event lebih mudah dijaga.
  • Debugging lebih langsung; satu input menghasilkan satu perubahan state yang bisa ditelusuri.
  • Konsistensi UI lebih mudah dipahami; apa yang ditampilkan biasanya dekat dengan event terbaru.

Namun sinkron cepat menjadi masalah ketika normalisasi event atau update graph memakan waktu. Gejalanya:

  • Cursor replay terasa tersendat.
  • Input drop atau tertunda saat ada lonjakan perubahan file.
  • Renderer kehilangan frame karena thread utama ikut sibuk.

Sinkron cocok jika:

  • Laju event tidak terlalu tinggi.
  • Transformasi per event ringan.
  • Replay lebih penting daripada ingestion real-time skala besar.

Async: throughput lebih baik, konsistensi lebih rumit

Pemrosesan async memisahkan penerimaan event dari pemrosesan berat. Event dapat dimasukkan ke buffer, queue, atau log append-only, lalu diproses worker di belakang. Keuntungannya:

  • Jalur ingest tetap ringan.
  • Backpressure lebih terkontrol.
  • Worker dapat diskalakan untuk indexing atau komputasi graph yang mahal.

Trade-off yang harus diterima:

  • Latensi end-to-end bisa bertambah karena event tidak langsung terlihat di replay.
  • Ordering lebih sulit, terutama jika ada banyak partisi, worker, atau sumber event.
  • Idempotensi dan deduplikasi menjadi penting.
  • Observability lebih kompleks karena bug bisa muncul dari delay, retry, atau event out-of-order.

Untuk replay session, async sering cocok jika Anda memisahkan dua mode:

  • Hot path: update minimal untuk interaktivitas, misalnya timeline dasar dan pointer event.
  • Cold path: enrichment, analisis graph, snapshot, kompresi, atau indexing tambahan.

Ini memungkinkan UI tetap responsif sambil pekerjaan berat berlangsung di belakang.

Contoh boundary event yang sehat

Daripada langsung mengirim seluruh state graph setiap kali ada perubahan, kirim event domain yang stabil dan kecil. Misalnya:

{
  "session_id": "sess-123",
  "seq": 1042,
  "ts": 1720000000,
  "type": "symbol_reference_changed",
  "file": "internal/parser/node.go",
  "symbol": "ParseNode",
  "payload": {
    "from": ["Parser", "Node"],
    "to": ["Parser", "ASTNode"]
  }
}

Kenapa ini membantu:

  • Payload lebih kecil daripada snapshot graph penuh.
  • Retry lebih aman karena event dapat dibuat idempoten dengan session_id + seq.
  • Storage event log lebih efisien untuk replay dan debugging.

Kesalahan umum adalah mengirim perubahan UI, perubahan AST, dan perubahan graph final dalam satu event campuran. Itu membuat kontrak sulit dipertahankan ketika model berubah.

Penyimpanan graph kode di memori vs database

Graph di memori: cepat, tetapi mudah mentok

Penyimpanan graph kode di memori cocok untuk eksplorasi lokal dan playback cepat. Ini sering menjadi pilihan awal yang rasional untuk sistem seperti Mindwalk karena:

  • Akses neighborhood dan traversal sangat cepat.
  • Model data lebih fleksibel saat skema graph masih berubah.
  • Implementasi lebih sederhana bila graph dibangun dari snapshot proyek lokal.

Masalahnya muncul saat codebase besar:

  • RAM membengkak karena node, edge, metadata simbol, dan indeks timeline hidup bersamaan.
  • Waktu startup naik karena graph harus dibangun atau dimuat penuh.
  • State sulit dibagikan antar proses atau antar pengguna.
  • Pemulihan setelah crash lebih buruk jika checkpoint tidak ada.

Pendekatan in-memory cocok bila:

  • Satu sesi aktif per mesin relatif kecil.
  • Query graph dominan bersifat lokal dan interaktif.
  • Anda bisa menerima rebuild dari sumber dasar saat aplikasi mulai.

Database: persistence dan sharing lebih baik, tetapi query harus disiplin

Ketika graph atau timeline perlu disimpan persisten, diakses lintas proses, atau dianalisis ulang, database menjadi pilihan logis. Namun “pakai database” bukan jawaban tunggal; yang penting adalah apa yang disimpan di database, dan apa yang tetap dijadikan cache in-memory.

Beberapa pola yang praktis:

  • Event log di database/object storage, graph kerja tetap di memori.
  • Snapshot graph periodik disimpan persisten, lalu diload sebagai baseline untuk replay.
  • Materialized view atau indeks turunan untuk query yang sering dipakai UI.

Keuntungan database:

  • Recovery lebih baik.
  • Multi-user dan multi-process lebih mudah.
  • Observability data lebih kuat karena histori tersimpan.

Biaya yang muncul:

  • Latensi query lebih tinggi dibanding pointer in-memory.
  • Kompleksitas skema dan migrasi meningkat.
  • Biaya operasional naik, terutama jika penyimpanan graph dan event tumbuh cepat.

Pola yang sering efektif: hybrid

Untuk codebase besar, arsitektur hybrid biasanya paling realistis:

  • Event mentah disimpan append-only untuk audit dan replay deterministik.
  • Snapshot graph disimpan periodik untuk mempercepat startup atau seek timeline.
  • Working set graph dan cache query di memori untuk interaksi UI.

Dengan pola ini, Anda tidak memaksa database melayani semua traversal real-time, tetapi juga tidak menggantungkan seluruh sistem pada RAM proses.

Anti-pattern yang sering muncul: memaksa database menjadi mesin replay real-time sekaligus source of truth sekaligus cache query. Hasilnya biasanya query lambat, skema kusut, dan tuning sulit.

Render lokal vs server-side atau streaming

Render lokal: default yang paling masuk akal

Untuk peta kode 3D, render lokal biasanya pilihan awal terbaik. Alasannya sederhana: visualisasi interaktif sangat sensitif terhadap latensi input, frame pacing, dan bandwidth. Jika graph dan timeline sudah tersedia di mesin pengguna, render lokal memberi:

  • Respons paling cepat untuk pan, zoom, scrub, dan highlight.
  • Biaya server lebih rendah karena GPU tidak dipusatkan.
  • Mode offline atau semi-offline lebih memungkinkan.

Kekurangannya:

  • Ketergantungan pada spesifikasi mesin pengguna.
  • Distribusi resource tidak merata; beberapa developer bisa nyaman, yang lain tidak.
  • Debugging masalah grafis lebih sulit karena sangat bergantung driver, OS, dan GPU setempat.

Server-side render atau streaming: untuk constraint tertentu, bukan default

Render server-side atau streaming bisa berguna jika:

  • Visualisasi harus diakses dari perangkat ringan.
  • Anda ingin lingkungan render konsisten.
  • Peta kode sangat berat dan mustahil diproses nyaman di banyak client.
  • Ada kebutuhan kolaborasi sinkron dengan state visual bersama.

Tetapi trade-off-nya signifikan:

  • Latensi interaksi naik karena semua input harus pergi-pulang jaringan.
  • Biaya operasional naik tajam jika butuh GPU server atau kapasitas encoding/streaming.
  • Observability lebih rumit karena bug bisa berasal dari render, encoder, jaringan, atau client.
  • Deployment lebih berat karena Anda mengelola pipeline compute grafis, bukan hanya backend data.

Karena itu, server-side render biasanya lebih cocok sebagai mode khusus, bukan mode utama. Misalnya untuk demo terkontrol, review sesi dari browser tipis, atau akses jarak jauh ke dataset besar yang tidak praktis diunduh.

Pendekatan tengah: server menyiapkan data, client tetap merender

Sering kali solusi paling sehat adalah server-side preprocessing, client-side rendering. Server bertugas menyiapkan snapshot, kompresi graph, layout awal, atau chunk data sesuai viewport/timeline. Client tetap merender interaksi final. Hasilnya:

  • Client tidak terbebani seluruh pipeline berat.
  • Interaksi tetap lebih responsif dibanding streaming frame video.
  • Biaya infrastruktur lebih moderat dibanding GPU rendering penuh di server.

Dampak pilihan arsitektur pada latensi, biaya, resource, observability, deployment, dan maintainability

Latensi

  • Monolith lokal + in-memory + render lokal: latensi interaktif terbaik, tetapi rawan terganggu oleh job berat dalam proses yang sama.
  • Service terpisah + async + database: lebih tahan terhadap beban, tetapi setiap boundary menambah delay.
  • Streaming render: paling sensitif terhadap kualitas jaringan.

Jika tujuan utama adalah playback yang terasa halus, minimalkan boundary pada jalur interaksi pengguna.

Biaya operasional

  • Monolith lokal paling murah dijalankan.
  • Queue, worker, database persisten, dan tracing menambah biaya tetap.
  • GPU server atau video streaming biasanya menjadi lonjakan biaya terbesar.

Biaya juga datang dari biaya manusia: semakin banyak komponen, semakin besar waktu untuk merawat deployment, insiden, dan kompatibilitas.

Konsumsi resource

  • In-memory graph mendorong penggunaan RAM.
  • Async processing memindahkan beban ke CPU worker dan storage queue.
  • Render lokal menekan GPU client; render server-side menekan GPU server dan bandwidth.

Pilihan yang baik adalah yang memindahkan beban ke resource yang paling murah dan paling mudah Anda kendalikan.

Observability

Pada sistem replay session, observability bukan hanya log error. Anda perlu bisa menjawab:

  • Event mana yang terlambat atau hilang?
  • Pada tahap mana ordering rusak?
  • Snapshot graph mana yang dipakai saat playback tertentu?
  • Apakah lag berasal dari query data atau rendering?

Minimal, siapkan:

  • Correlation ID per session dan sequence number per event.
  • Pengukuran durasi per stage: ingest, normalize, index, query, render.
  • Mode replay deterministik dari event log untuk reproduksi bug.

Kompleksitas deployment

Monolith lokal bisa dikirim sebagai satu binary atau satu aplikasi desktop. Service terpisah menambah kebutuhan seperti:

  • Konfigurasi environment per service.
  • Manajemen secret.
  • Database lifecycle dan backup.
  • Versi kontrak API/event.
  • Monitoring dan alerting lintas komponen.

Jika tim Anda masih kecil, kompleksitas ini bisa lebih mahal daripada keuntungan skalanya.

Maintainability

Maintainability bukan soal sedikitnya file atau service, melainkan seberapa jelas boundary dan invariants sistem. Arsitektur sederhana yang modular sering lebih mudah dirawat daripada microservice yang dipisah tanpa alasan beban nyata. Pisahkan komponen ketika:

  • Ownership jelas.
  • Beban dan lifecycle berbeda.
  • Kontrak datanya stabil.
  • Kegagalan satu komponen memang perlu diisolasi.

Anti-pattern yang sering muncul

1. Memecah service terlalu dini

Masalah utamanya bukan teknologi jaringan, tetapi kehilangan kemampuan debug cepat. Saat model event dan graph masih bergerak, kontrak antar service mudah berubah dan memperlambat iterasi.

2. Menyimpan snapshot penuh terlalu sering

Ini membuat storage membengkak dan I/O berat. Lebih baik simpan event append-only dan snapshot periodik di titik yang benar-benar berguna untuk seek atau recovery.

3. Menjadikan UI sebagai source of truth

State visual seharusnya turunan dari state domain replay, bukan sebaliknya. Jika tidak, bug sinkronisasi antara timeline, graph, dan tampilan akan sulit direproduksi.

4. Mengabaikan ordering dan idempotensi event

Pada pipeline async, event duplikat atau out-of-order adalah kejadian normal. Jika sistem mengasumsikan “sekali kirim pasti beres”, replay akan rusak secara halus dan sulit dideteksi.

5. Mencampur query interaktif dengan job analitik berat pada store yang sama

Akibatnya, playback UI terkena dampak dari komputasi non-interaktif. Pisahkan jalur kerja atau setidaknya pisahkan prioritas dan cache-nya.

Kapan arsitektur sederhana sudah cukup

Pilih arsitektur sederhana jika sebagian besar kondisi ini masih benar:

  • Satu pengguna atau sedikit pengguna aktif.
  • Replay berasal dari data lokal, bukan ingest terpusat.
  • Graph kode masih nyaman dimuat di memori.
  • Tidak ada kebutuhan kolaborasi real-time yang berat.
  • Masalah utama masih akurasi visualisasi dan UX playback, bukan scale-out.

Rekomendasi praktiknya:

  • Mulai dari modular monolith lokal.
  • Simpan event log sederhana dan sequence number yang tegas.
  • Gunakan snapshot periodik seperlunya.
  • Profilkan memori, CPU, dan frame time sebelum memecah komponen.

Kapan perlu dipisah

Mulai pisahkan komponen jika Anda menemukan salah satu sinyal berikut:

  • Ingestion terganggu oleh indexing atau render.
  • Replay membutuhkan query graph yang dipakai banyak proses/client.
  • Ukuran sesi dan graph membuat startup atau seek terlalu lambat.
  • Tim mulai membutuhkan observability lebih kuat dan recovery yang konsisten.
  • Resource rendering atau pemrosesan perlu dialokasikan terpisah.

Urutan pemisahan yang biasanya paling masuk akal:

  1. Pisahkan worker pemrosesan event berat.
  2. Tambahkan storage persisten untuk event log dan snapshot.
  3. Pisahkan graph/query service jika akses sudah lintas proses.
  4. Evaluasi mode streaming hanya jika render lokal benar-benar tidak cukup.

Checklist keputusan untuk tim kecil sampai produk yang mulai tumbuh

Checklist untuk tim kecil

  • Apakah satu proses lokal masih cukup responsif saat membuka codebase terbesar yang umum dipakai?
  • Apakah event replay dapat diproses sinkron tanpa mengganggu interaksi?
  • Apakah bug bisa direproduksi dari event log lokal?
  • Apakah graph masih masuk akal di RAM tanpa memaksa optimasi prematur?
  • Apakah deployment satu aplikasi lebih berharga daripada skala horizontal?

Jika jawabannya mayoritas “ya”, pertahankan arsitektur sederhana.

Checklist untuk produk yang mulai tumbuh

  • Apakah ada bottleneck jelas antara ingest, processing, storage, dan render?
  • Apakah beberapa komponen membutuhkan scaling atau resource class berbeda?
  • Apakah data replay perlu dipakai lintas mesin, lintas tim, atau lintas sesi?
  • Apakah query graph dan playback mulai butuh cache, snapshot, atau indeks khusus?
  • Apakah observability saat ini tidak cukup untuk menemukan sumber lag atau data mismatch?
  • Apakah biaya kompleksitas tambahan lebih kecil daripada biaya masalah performa sekarang?

Jika beberapa jawaban “ya”, mulai pecah komponen secara bertahap, bukan sekaligus.

Rekomendasi arsitektur praktis berbasis tahap

Tahap 1: prototipe serius

  • Modular monolith lokal.
  • Event diproses sinkron atau semi-sinkron.
  • Graph utama di memori.
  • Render lokal.
  • Event log disimpan untuk debugging.

Tahap 2: beban mulai nyata

  • Ingest tetap ringan.
  • Pemrosesan enrichment dan snapshot dipindah ke worker async.
  • Event log dan snapshot disimpan persisten.
  • Client tetap render lokal, tetapi data bisa diambil bertahap.

Tahap 3: produk tumbuh

  • Graph/query service dipisah jika dibutuhkan banyak consumer.
  • Cache in-memory dan persistence dipadukan.
  • Tracing lintas komponen mulai wajib.
  • Render server-side hanya untuk use case khusus, bukan default.

Penutup

Dalam konteks Mindwalk dan tool replay sesi coding pada peta kode 3D, keputusan arsitektur terbaik biasanya adalah yang sesederhana mungkin di jalur interaktif, dan sekompleks yang diperlukan di jalur pemrosesan berat. Monolith lokal, pemrosesan sinkron, graph di memori, dan render lokal sering kali cukup untuk tahap awal yang serius. Saat skala naik, pecah berdasarkan bottleneck nyata: worker async untuk indexing, persistence untuk event log dan snapshot, lalu service query bila data perlu dibagi lebih luas.

Jika Anda ragu, mulai dari pertanyaan ini: bagian mana yang paling mahal jika salah—latensi interaksi, biaya infrastruktur, atau kecepatan iterasi tim? Jawaban atas pertanyaan itu biasanya lebih berguna daripada debat generik soal monolith versus microservice.