Menggali Lebih Dalam Event Sourcing dan CQRS: Fondasi Sistem yang Auditabel dan Skalabel
Halo, para developer! Pernahkah Anda merasa frustrasi saat mencoba melacak setiap perubahan data di aplikasi Anda? Atau mungkin Anda menghadapi masalah performa ketika harus menampilkan data yang sama dalam berbagai format yang berbeda? Jika ya, Anda tidak sendirian. Aplikasi web modern seringkali menghadapi tantangan ini, terutama ketika mereka tumbuh dan menjadi lebih kompleks.
Dalam artikel sebelumnya, kita sudah membahas tentang [Event-Driven Architecture (EDA)](Event-Driven Architecture (EDA): Membangun Aplikasi Responsif dan Skalabel) yang membantu kita membangun sistem yang lebih responsif dan skalabel dengan berinteraksi melalui event. Nah, hari ini kita akan melangkah lebih jauh, menyelami dua pola arsitektur yang seringkali menjadi “pasangan serasi” di dunia event-driven: Event Sourcing (ES) dan Command Query Responsibility Segregation (CQRS).
Kedua pola ini mungkin terdengar rumit, tetapi mereka menawarkan solusi elegan untuk masalah auditabilitas, skalabilitas, dan fleksibilitas data yang sering menghantui aplikasi kita. Mari kita bongkar satu per satu!
1. Pendahuluan: Mengapa Kita Butuh Event Sourcing dan CQRS?
Dalam model aplikasi tradisional, kita biasanya menyimpan state atau status terakhir dari sebuah entitas. Misalnya, jika Anda punya akun bank, yang disimpan di database adalah saldo terakhir Anda. Ketika ada transaksi, saldo itu langsung di-update.
Masalahnya?
- Kehilangan Sejarah: Kita tahu saldo terakhirnya, tapi sulit untuk tahu bagaimana saldo itu terbentuk. Jika ada kesalahan, melacak penyebabnya bisa jadi mimpi buruk.
- Performa Query: Seringkali, model data yang optimal untuk menyimpan (menulis) tidak optimal untuk membaca (query). Anda mungkin butuh data yang sama dalam berbagai bentuk (misal: tampilan detail produk, daftar produk di keranjang, laporan penjualan), yang seringkali membutuhkan query kompleks atau denormalisasi yang merepotkan.
- Auditabilitas: Bagaimana jika regulator meminta catatan lengkap setiap perubahan status suatu pesanan? Menyimpan log di tabel terpisah itu satu hal, tapi menjadikannya sebagai sumber kebenaran utama adalah hal lain.
Di sinilah Event Sourcing dan CQRS masuk. Mereka bukan sekadar “tambahan” pada aplikasi Anda, melainkan cara berpikir ulang tentang bagaimana data disimpan dan diakses. Dengan mengadopsi pola ini, kita bisa membangun sistem yang tidak hanya lebih skalabel dan performan, tetapi juga memiliki jejak audit yang tak terbantahkan.
2. Memahami Event Sourcing: Bukan Sekadar Logging Biasa
📌 Apa itu Event Sourcing?
Bayangkan Anda memiliki buku besar akuntansi. Setiap transaksi — uang masuk, uang keluar — dicatat sebagai entri baru yang tidak dapat diubah. Saldo akhir Anda adalah hasil dari semua entri tersebut yang dihitung secara berurutan.
Itulah inti dari Event Sourcing. Alih-alih menyimpan status terakhir dari sebuah objek (misalnya, saldo akun bank), Event Sourcing menyimpan semua event (kejadian) yang menyebabkan perubahan pada objek tersebut. Setiap kali ada sesuatu yang terjadi, sebuah “event” baru dicatat dan ditambahkan ke “event store” (penyimpanan event) sebagai fakta yang tidak dapat diubah. State saat ini dari objek tersebut kemudian “dibangun ulang” dengan memutar ulang (replay) semua event secara berurutan.
Analogi Konkret: Anggaplah Anda punya akun bank.
- Model Tradisional: Database menyimpan
saldo: Rp 1.000.000. Jika Anda menarik Rp 200.000, database langsung mengupdate menjadisaldo: Rp 800.000. Anda kehilangan informasi transaksi penarikan itu sendiri sebagai sumber kebenaran utama. - Event Sourcing: Database menyimpan
Event: AkunDibuat(saldoAwal: Rp 1.000.000), laluEvent: UangDitarik(jumlah: Rp 200.000). Untuk mengetahui saldo saat ini, sistem akan memproses kedua event tersebut secara berurutan:Rp 1.000.000 - Rp 200.000 = Rp 800.000.
💡 Komponen Utama Event Sourcing:
- Events: Fakta yang tidak dapat diubah (immutable) tentang sesuatu yang terjadi di masa lalu. Contoh:
PesananDibuat,ItemDitambahkanKeKeranjang,PembayaranBerhasil. - Event Store: Database khusus yang dirancang untuk menyimpan event secara berurutan dan efisien. Ini adalah sumber kebenaran utama aplikasi Anda.
- Aggregates: Unit konsistensi dalam domain Anda. Mereka menerima perintah (commands), memvalidasi, dan menghasilkan event. Event-event ini kemudian disimpan di Event Store.
Contoh Struktur Event (konseptual):
// Event: PesananDibuat
{
"eventId": "e6a7b8c9-d0e1-4f2a-b3c4-d5e6f7a8b9c0",
"timestamp": "2023-10-27T10:00:00Z",
"eventType": "PesananDibuat",
"payload": {
"orderId": "ORD-001",
"userId": "user-A",
"items": [
{"productId": "prod-X", "qty": 2, "price": 100000}
]
},
"metadata": {
"source": "WebApp",
"ipAddress": "192.168.1.1"
}
}
// Event: ItemDitambahkanKePesanan
{
"eventId": "f1g2h3i4-j5k6-7l8m-9n0o-1p2q3r4s5t6u",
"timestamp": "2023-10-27T10:05:00Z",
"eventType": "ItemDitambahkanKePesanan",
"payload": {
"orderId": "ORD-001",
"productId": "prod-Y",
"qty": 1,
"price": 50000
},
"metadata": {
"source": "WebApp",
"ipAddress": "192.168.1.1"
}
}
// ... event lainnya yang mengubah state pesanan ORD-001
✅ Keuntungan Event Sourcing:
- Auditabilitas Lengkap: Anda memiliki riwayat lengkap dan tak terbantahkan dari setiap perubahan. Sangat berharga untuk kepatuhan regulasi atau debugging.
- Time-Travel Debugging: Bisa “memutar ulang” aplikasi ke state apapun di masa lalu hanya dengan memutar ulang event hingga titik waktu tertentu.
- Rekonstitusi State: State saat ini bisa dibangun ulang kapan saja. Ini memungkinkan Anda mengubah cara Anda menginterpretasikan event di masa depan tanpa kehilangan data lama.
- Decoupling: Event adalah kontrak yang stabil, memungkinkan bagian-bagian sistem yang berbeda berinteraksi tanpa terlalu terikat.
⚠️ Tantangan Event Sourcing:
- Kompleksitas Awal: Kurva pembelajaran yang curam dan membutuhkan pola pikir yang berbeda.
- Event Versioning: Bagaimana jika skema event berubah? Ini perlu ditangani dengan hati-hati (misalnya, dengan upcaster).
- Snapshotting: Untuk Aggregate yang memiliki banyak event, memutar ulang semua event setiap saat bisa lambat. Snapshotting (menyimpan state pada titik tertentu) membantu mempercepat rekonstitusi.
3. Memecah Monolit Data dengan CQRS (Command Query Responsibility Segregation)
🎯 Apa itu CQRS?
CQRS adalah pola arsitektur yang memisahkan model untuk operasi menulis data (Commands) dan operasi membaca data (Queries). Artinya, Anda tidak lagi menggunakan satu model data atau database yang sama untuk kedua jenis operasi tersebut.
Analogi Konkret: Bayangkan sebuah restoran.
- Command Side (Dapur): Ini adalah tempat pesanan (Commands) diterima, diproses, dan menghasilkan hidangan (Events). Fokusnya adalah memastikan setiap pesanan diproses dengan benar dan bahan baku digunakan secara konsisten.
- Query Side (Ruang Makan/Menu): Ini adalah tempat hidangan disajikan kepada pelanggan (Queries). Fokusnya adalah menyajikan hidangan dengan cepat dan dalam berbagai bentuk (menu, daftar spesial harian, dll.) tanpa harus tahu detail proses memasaknya.
Command Side (Write Model)
- Menerima Commands (perintah yang mengubah state, misalnya
BuatPesanan,UpdateStokProduk). - Memvalidasi Commands, menjalankan logika bisnis, dan menghasilkan Events (fakta bahwa sesuatu telah terjadi).
- Events ini kemudian disimpan ke Event Store (jika Anda juga menggunakan Event Sourcing) atau langsung mengubah state di database write model.
- Fokus utama di sini adalah konsistensi data.
Query Side (Read Model)
- Mendengarkan Events yang terjadi di Command Side (atau perubahan data langsung dari write model).
- Membangun dan mengupdate proyeksi (atau materialized views) yang dioptimalkan khusus untuk query. Proyeksi ini seringkali berupa database yang berbeda atau bahkan jenis database yang berbeda (misalnya, NoSQL untuk performa baca).
- Ketika aplikasi membutuhkan data, ia akan melakukan Queries ke Read Model ini.
- Fokus utama di sini adalah performa dan fleksibilitas query.
✅ Keuntungan CQRS:
- Skalabilitas Independen: Anda bisa menskalakan Command Side dan Query Side secara terpisah sesuai kebutuhan. Jika banyak user yang hanya membaca, Anda bisa menambahkan lebih banyak instance untuk Read Model tanpa membebani Write Model.
- Optimasi Performa Query: Read Model dapat dioptimalkan secara ekstrim untuk kebutuhan query spesifik. Anda bisa menggunakan database yang berbeda (misal, SQL untuk Write, NoSQL seperti ElasticSearch atau Redis untuk Read).
- Fleksibilitas Model Data: Model data untuk menulis bisa sangat berbeda dari model data untuk membaca. Ini memungkinkan evolusi yang lebih mudah.
- Evolusi Mudah: Jika kebutuhan query berubah, Anda bisa membangun Read Model baru dari Event Store tanpa memengaruhi Write Model atau Read Model yang sudah ada.
⚠️ Tantangan CQRS:
- Konsistensi Eventual: Data di Read Model mungkin tidak seketika konsisten dengan Write Model. Ada jeda waktu (latency) saat event diproses untuk mengupdate Read Model. Ini perlu dipertimbangkan dalam desain UX.
- Kompleksitas Infrastruktur: Membutuhkan lebih banyak komponen (Event Store, Message Broker untuk distribusi event, beberapa database).
- Sinkronisasi: Memastikan Read Model selalu terupdate dan konsisten dengan Write Model.
4. Sinergi Event Sourcing dan CQRS: Kombinasi Kuat
Ketika Event Sourcing dan CQRS digabungkan, mereka membentuk sebuah arsitektur yang sangat kuat dan fleksibel. Event Sourcing menyediakan sumber kebenaran yang tak terbantahkan (Event Store), sementara CQRS memanfaatkan event-event ini untuk membangun model baca yang sangat dioptimalkan.
Bagaimana Keduanya Bekerja Sama:
- Command Diterima: Aplikasi menerima Command (misal:
BuatPesanan). - Aggregate Memproses: Aggregate yang relevan (misal: Aggregate
Pesanan) memvalidasi Command dan menghasilkan satu atau lebih Events (misal:PesananDibuat). - Events Disimpan: Events ini disimpan secara berurutan ke Event Store.
- Events Dipublikasikan: Setelah disimpan, Events dipublikasikan (melalui Message Broker seperti Kafka atau RabbitMQ) agar dapat didengarkan oleh komponen lain.
- Read Models Mengkonsumsi: Read Models (proyeksi) mendengarkan Events ini. Setiap Read Model memproses Events yang relevan untuk membangun dan mengupdate datanya sendiri, yang dioptimalkan untuk query spesifik.
- Query ke Read Models: Ketika aplikasi membutuhkan data, ia melakukan Query ke Read Model yang sesuai.
Diagram Alur Kerja Sederhana:
[User Interface] --Command--> [Command Handler] --> [Aggregate] --> [Event Store]
|
V
[Event Bus/Message Broker]
|
V
[Read Model Projector 1] <---Events--- [Read Model Projector 2]
| |
V V
[Read Database 1] [Read Database 2]
^ ^
| |
--Query--> [User Interface] <--Query--
Contoh: Aplikasi E-commerce
- Command:
TambahProdukKeKeranjang(userId, productId, qty) - Aggregate:
KeranjangBelanja - Event:
ProdukDitambahkanKeKeranjang(userId, productId, qty, timestamp) - Event Store: Menyimpan event
ProdukDitambahkanKeKeranjang. - Read Model 1:
KeranjangBelanjaView: Mendengarkan eventProdukDitambahkanKeKeranjangdan mengupdate tabelkeranjang_belanjayang dioptimalkan untuk menampilkan isi keranjang user dengan cepat (misal, denormalisasi semua detail produk langsung ke tabel ini). - Read Model 2:
DaftarOrderAdminView: Mendengarkan event yang sama, tapi mungkin mengupdate tabeldaftar_order_adminyang berisi ringkasan semua pesanan, dioptimalkan untuk filter dan pencarian oleh admin.
Dengan kombinasi ini, Anda mendapatkan sistem yang sangat fleksibel: setiap perubahan state direkam secara permanen (Event Sourcing), dan data dapat disajikan dalam berbagai format yang sangat dioptimalkan untuk query (CQRS).
5. Kapan Menggunakan Event Sourcing dan CQRS?
Meskipun kuat, Event Sourcing dan CQRS bukanlah silver bullet untuk setiap masalah. Menerapkannya secara tidak tepat bisa menambah kompleksitas yang tidak perlu.
✅ Cocok untuk:
- Sistem dengan Kebutuhan Auditabilitas Tinggi: Keuangan, logistik, atau sistem regulasi lainnya yang memerlukan jejak audit yang tak terbantahkan.
- Sistem yang Membutuhkan Riwayat Lengkap: Untuk analisis data, time-travel debugging, atau kemampuan untuk “memutar ulang” dan merekonstitusi state kapan saja.
- Aplikasi dengan Kebutuhan Skalabilitas Tinggi: Di mana operasi tulis dan baca memiliki pola yang sangat berbeda dan membutuhkan optimasi independen.
- Domain Kompleks: Di mana model data untuk menulis dan membaca sangat berbeda, dan model CRUD tradisional menjadi sulit dikelola.
- Arsitektur Microservices: Memfasilitasi decoupling antar layanan dan evolusi independen.
❌ Tidak Cocok untuk:
- Aplikasi CRUD Sederhana: Untuk aplikasi yang hanya melakukan Create, Read, Update, Delete sederhana, overhead kompleksitas ES/CQRS jauh lebih besar daripada manfaatnya.
- Proyek dengan Anggaran/Waktu Terbatas dan Tim Kecil: Membutuhkan investasi waktu dan keahlian yang signifikan untuk implementasi dan pemeliharaan.
- Ketika Konsistensi Seketika Mutlak Diperlukan: Jika aplikasi Anda tidak bisa mentolerir konsistensi eventual (bahkan dalam milidetik), maka ES/CQRS mungkin bukan pilihan terbaik (meskipun ada cara untuk mengatasinya, itu menambah kompleksitas).
6. Tips Praktis dan Best Practices
💡 Jika Anda memutuskan untuk menyelami Event Sourcing dan CQRS, berikut beberapa tips untuk membantu perjalanan Anda:
- Mulai dari yang Kecil: Jangan coba menerapkan ES/CQRS ke seluruh monolit Anda sekaligus. Identifikasi satu domain yang paling kompleks atau paling membutuhkan auditabilitas/skalabilitas, dan terapkan di sana terlebih dahulu.
- Event Naming yang Jelas: Pastikan nama event Anda jelas, dalam bentuk lampau (past tense), dan berpusat pada domain (misal:
PesananDibuat, bukanOrderCreated). Ini membuat event mudah dipahami dan menjadi bahasa ubiquitous Anda. - Event Versioning: Skema event pasti akan berevolusi. Rencanakan strategi event versioning sejak awal (misalnya, dengan menambahkan nomor versi ke event atau menggunakan upcaster).
- Snapshotting: Untuk Aggregate yang memiliki riwayat event yang sangat panjang, implementasikan snapshotting secara berkala untuk mempercepat proses rekonstitusi state.