Client-Side CQRS: Membangun Frontend yang Responsif dan Skalabel dengan Pemisahan Command dan Query
1. Pendahuluan
Sebagai developer web, kita sering berhadapan dengan kompleksitas dalam mengelola data dan state di aplikasi frontend. Dari sekadar menampilkan data hingga mengelola interaksi pengguna yang kompleks, aplikasi modern menuntut responsivitas tinggi, skalabilitas, dan kemudahan perawatan. Terlebih lagi, dengan tren aplikasi offline-first dan real-time, tantangannya semakin bertambah.
Pernahkah kamu merasa logika untuk mengubah data (misalnya, menambahkan item ke keranjang belanja) bercampur aduk dengan logika untuk menampilkan data (misalnya, menghitung total harga keranjang)? Atau mungkin kamu kesulitan mengoptimalkan performa UI karena model data yang sama digunakan untuk menulis dan membaca, padahal kebutuhan keduanya sangat berbeda?
Di ranah backend, pola Command-Query Responsibility Segregation (CQRS) sudah cukup dikenal untuk mengatasi masalah serupa. CQRS memisahkan operasi yang mengubah state (Command) dari operasi yang membaca state (Query). Tapi, bagaimana jika kita menerapkan pola yang sama di sisi klien?
Artikel ini akan membawa kamu menyelami konsep Client-Side CQRS, mengapa ini penting, bagaimana arsitekturnya, serta contoh konkret penerapannya untuk membangun aplikasi web yang lebih responsif, skalabel, dan mudah dikelola.
2. Memahami CQRS di Konteks Frontend
Secara sederhana, CQRS adalah pola arsitektur yang memisahkan model untuk menulis data (Command Model) dari model untuk membaca data (Query Model).
📌 Ingat Konsep Dasar CQRS:
- Command: Representasi dari aksi yang mengubah state. Contoh:
AddProductToCartCommand,UpdateUserProfileCommand. Command seharusnya tidak mengembalikan data, hanya status keberhasilan/kegagalan. - Query: Representasi dari permintaan untuk membaca data. Contoh:
GetCartItemsQuery,GetUserProfileQuery. Query seharusnya tidak mengubah state, hanya mengembalikan data.
Di sisi frontend, pemisahan ini berarti kita bisa memiliki struktur data yang berbeda dan dioptimalkan secara spesifik untuk masing-masing tujuan.
❌ Tanpa CQRS: Seringkali kita menggunakan satu model data (misalnya, state React atau objek di Redux) untuk kedua tujuan. Ini bisa menyebabkan:
- Model menjadi gemuk dan kompleks karena harus memenuhi kebutuhan baca dan tulis.
- Logika validasi dan mutasi bercampur dengan logika presentasi.
- Sulit mengoptimalkan performa karena perubahan pada satu bagian model memengaruhi bagian lain yang tidak terkait.
✅ Dengan Client-Side CQRS:
- Command Model: Bisa berupa objek data yang sederhana, fokus pada input yang diperlukan untuk operasi mutasi. Logika validasi dan perubahan state akan ditangani oleh “handler” untuk command tersebut. Data ini mungkin hanya ada sementara sebelum dikirim ke backend atau di-persist lokal.
- Query Model: Ini adalah representasi state yang dioptimalkan untuk ditampilkan di UI. Bisa berupa struktur data yang di-denormalisasi, di-cache, atau bahkan “views” khusus untuk komponen UI tertentu. Model ini diperbarui sebagai respons terhadap event (dari command lokal yang berhasil, atau dari backend).
3. Manfaat Menerapkan Client-Side CQRS di Frontend
Mengadopsi CQRS di frontend bukan tanpa biaya, namun manfaatnya sangat terasa terutama untuk aplikasi kompleks:
-
Responsivitas UI yang Unggul:
- 💡 Optimistic UI: Command dapat langsung memicu pembaruan Query Model secara optimistik (tanpa menunggu respons backend). Jika backend gagal, perubahan bisa di-rollback. Ini memberikan pengalaman pengguna yang terasa instan.
- UI dapat diperbarui lebih cepat karena Query Model dioptimalkan untuk dibaca dan tidak terbebani oleh logika mutasi.
-
Skalabilitas dan Fleksibilitas:
- Kamu bisa memiliki banyak Query Model yang berbeda untuk berbagai tampilan UI, semuanya berasal dari satu set Command/Event. Misalnya, satu Query Model untuk daftar produk, satu lagi untuk detail produk, dan satu lagi untuk ringkasan di keranjang.
- Memudahkan adaptasi dengan backend yang juga event-driven atau berbasis CQRS.
-
Maintainability dan Keterbacaan Kode yang Lebih Baik:
- Logika bisnis untuk mutasi (Command Handler) terpisah dari logika presentasi (Query Handler/UI).
- Setiap bagian memiliki tanggung jawab yang jelas, mengurangi cognitive load developer.
- Memudahkan testing karena unit-unit logika lebih terisolasi.
-
Dukungan Kuat untuk Aplikasi Offline-First:
- Command dapat di-persist secara lokal (misalnya di IndexedDB) dan di-queue untuk dikirim ke backend saat koneksi kembali.
- Query Model dapat terus melayani UI dari data lokal yang tersedia, memberikan pengalaman yang mulus bahkan tanpa jaringan.
- Mempermudah penanganan konflik data karena Command dan Event yang jelas.
-
Auditability:
- Dengan menyimpan “event” dari setiap Command yang berhasil di-persist, kita bisa memiliki jejak perubahan state yang lengkap (Event Sourcing). Ini berguna untuk debugging, analisis, atau bahkan fitur “undo/redo” yang kompleks.
4. Arsitektur Client-Side CQRS
Mari kita bedah komponen-komponen utama dalam arsitektur Client-Side CQRS:
- Command: Objek yang mendeskripsikan niat pengguna untuk mengubah state.
- Command Bus / Dispatcher: Menerima Command dari UI, melakukan validasi awal, dan meneruskannya ke Command Handler yang sesuai.
- Command Handler: Logika inti yang memproses Command. Ini akan:
- Melakukan validasi bisnis lebih lanjut.
- Mengubah Command Model (jika ada, atau langsung berinteraksi dengan penyimpanan data).
- Berinteraksi dengan API backend untuk persistensi data di server.
- Setelah berhasil, bisa memancarkan Event lokal (misalnya,
ProductAddedToCartEvent). - Jika gagal, menangani error dan mungkin memicu rollback optimistic UI.
- Local Data Store (Command Model / Event Store):
- Ini bisa berupa IndexedDB untuk menyimpan Command yang di-queue (saat offline) atau Event yang dihasilkan dari Command yang berhasil.
- Untuk aplikasi offline-first, ini sangat krusial.
- Query Store / UI State:
- Ini adalah model data yang dioptimalkan khusus untuk kebutuhan UI. Bisa berupa state management library seperti Redux store, Zustand, Recoil, atau bahkan hanya state lokal komponen React.
- Query Store diperbarui sebagai respons terhadap:
- Event Lokal: Dari Command Handler yang berhasil. Ini adalah jalur untuk optimistic UI.
- Data dari Backend: Setelah sinkronisasi atau fetching data baru dari API.
- Query Handler / Selector:
- Logika untuk mengambil dan memfilter data dari Query Store agar sesuai dengan kebutuhan tampilan UI.
- Contoh: selector di Redux,
useQueryhooks di React Query yang mengambil dari cache lokal.
- Synchronization Layer:
- Bertanggung jawab untuk mengirim Command yang di-queue ke backend saat online.
- Bertanggung jawab untuk memastikan Query Store tetap sinkron dengan data terbaru dari backend (misalnya, melalui WebSockets, Server-Sent Events, atau polling berkala).
graph TD
A[UI Component] -->|Dispatch Command| B(Command Bus)
B --> C{Command Handler}
C -->|Validate & Mutate| D(Local Data Store - Command/Event Model)
D --> E(Backend API)
E --> F{Response / Event from Backend}
F --> G(Synchronization Layer)
C -->|Emit Local Event| H(Event Stream)
H --> I(Query Store / UI State)
G --> I
A -->|Request Data (Query)| J(Query Handler / Selector)
J -->|Read Optimized Data| I
I --> A
subgraph Client-Side
B
C
D
H
G
I
J
end
subgraph Backend
E
F
end
5. Contoh Konkret: Aplikasi E-commerce Offline-First
Bayangkan aplikasi e-commerce sederhana dengan fitur keranjang belanja yang bisa digunakan offline.
Skenario: Menambahkan Produk ke Keranjang (Offline & Optimistik)
-
UI Component (Product Card): Pengguna mengklik tombol “Tambah ke Keranjang”.
- Menciptakan
AddProductToCartCommand(misalnya:{ type: 'ADD_TO_CART', productId: 'p123', quantity: 1 }). - Mengirim Command ini ke Command Bus.
- Menciptakan
-
Command Bus: Menerima
AddProductToCartCommand.- Meneruskannya ke
AddProductToCartCommandHandler.
- Meneruskannya ke
-
AddProductToCartCommandHandler:
- ✅ Optimistik Update: Segera memancarkan
ProductAddedToCartEventlokal. - Menyimpan
AddProductToCartCommandini ke IndexedDB sebagai Command yang di-queue. - Mencoba mengirim Command ke Backend API (
/api/cart/add).
- ✅ Optimistik Update: Segera memancarkan
-
Query Store / UI State (Redux/Zustand Store):
- Menerima
ProductAddedToCartEventlokal. - Mengupdate state keranjang belanja di Query Store (misalnya, menambahkan item secara langsung).
- UI yang menampilkan keranjang belanja akan langsung me-render ulang dengan item baru.
- Menerima
-
Synchronization Layer:
- Jika
AddProductToCartCommandberhasil dikirim ke backend, Command tersebut dihapus dari IndexedDB. - Jika gagal (karena offline atau error server), Command tetap di IndexedDB. Layer ini secara periodik atau saat koneksi pulih, mencoba mengirim ulang Command yang tertunda.
- Backend mungkin juga memancarkan Event (misalnya, melalui WebSockets) yang diterima oleh Synchronization Layer dan kemudian digunakan untuk mengupdate Query Store, memastikan konsistensi akhir.
- Jika
Skenario: Menampilkan Item Keranjang
-
UI Component (Cart Page): Membutuhkan daftar item di keranjang.
- Mengirim
GetCartItemsQueryke Query Handler.
- Mengirim
-
Query Handler:
- Mengambil data item keranjang dari Query Store (yang sudah dioptimalkan untuk tampilan).
- Mengembalikan data tersebut ke UI Component.
- UI akan menampilkan data yang cepat dan responsif, bahkan jika Command yang tertunda masih ada di IndexedDB.
Dengan pendekatan ini, pengalaman pengguna akan sangat responsif karena UI langsung bereaksi terhadap aksi, sementara sinkronisasi dengan backend dan penanganan offline terjadi di latar belakang.
6. Tantangan dan Pertimbangan
Meskipun kuat, Client-Side CQRS membawa beberapa tantangan:
- Kompleksitas Awal: Membangun Command Bus, Handlers, dan manajemen Event membutuhkan boilerplate dan pemahaman konsep yang lebih dalam di awal.
- Sinkronisasi dan Eventual Consistency: Menjaga Query Store tetap sinkron dengan backend, terutama dalam skenario offline dan penanganan konflik, adalah bagian yang paling menantang. Kamu perlu strategi untuk:
- Deduplikasi Event: Mencegah Query Store diperbarui berkali-kali dari event lokal dan backend yang sama.
- Penanganan Konflik: Jika user melakukan perubahan offline, lalu backend juga berubah, bagaimana cara menggabungkannya? (CRDTs bisa jadi solusi, tapi menambah kompleksitas).
- Ukuran Bundle: Jika kamu mengimplementasikan CQRS dengan banyak library atau framework, ukuran bundle bisa bertambah. Pertimbangkan solusi yang ringan atau kustom.
- Kapan Menggunakannya? CQRS tidak selalu diperlukan untuk setiap aplikasi. Ini paling cocok untuk:
- Aplikasi dengan logika bisnis kompleks dan banyak mutasi data.
- Aplikasi yang membutuhkan optimistic UI dan responsivitas tinggi.
- Aplikasi offline-first atau yang berinteraksi dengan backend event-driven.
- Aplikasi dengan kebutuhan performa baca yang sangat spesifik (misalnya, banyak jenis laporan atau tampilan data yang berbeda).
🎯 Tips Praktis:
- Mulailah dengan memisahkan Command dan Query secara konseptual terlebih dahulu, bahkan jika kamu masih menggunakan satu state store.
- Gunakan TypeScript untuk mendefinisikan Command dan Event agar lebih terstruktur dan type-safe.
- Manfaatkan IndexedDB atau library seperti Dexie.js untuk manajemen data lokal yang tangguh.
- Pertimbangkan library state management yang fleksibel seperti Zustand atau Recoil yang memungkinkan kamu membangun logika Command/Query di atasnya.
Kesimpulan
Client-Side CQRS adalah pola arsitektur yang powerful untuk membangun aplikasi web yang lebih responsif, skalabel, dan mudah dirawat. Dengan memisahkan secara jelas antara operasi yang mengubah data (Command) dan operasi yang membaca data (Query), kita dapat mengoptimalkan setiap bagian aplikasi secara independen, memberikan pengalaman pengguna yang unggul, dan mendukung fitur-fitur canggih seperti offline-first.
Meskipun ada kurva pembelajaran di awal, investasi dalam pola ini akan sangat terbayar di kemudian hari, terutama untuk proyek-proyek dengan kompleksitas tinggi atau kebutuhan performa yang ekstrem. Mulailah dengan memahami prinsip dasarnya, lalu terapkan secara bertahap di bagian-bagian aplikasi yang paling membutuhkan. Frontend kamu akan berterima kasih!
🔗 Baca Juga
- Menggali Lebih Dalam Event Sourcing dan CQRS: Fondasi Sistem yang Auditabel dan Skalabel
- Membangun Aplikasi Offline-First & Real-time dengan IndexedDB dan WebSockets: Pola Sinkronisasi Data yang Tangguh
- Memilih Strategi State Management yang Tepat untuk Aplikasi Web Modern: Panduan Pragmatis
- Optimistic UI Tingkat Lanjut: Strategi Penanganan Konflik dan Error untuk Pengalaman Pengguna yang Mulus