Node.js ve MongoDB ile Event Sourcing & CQRS: Modern Uygulamalarınızda Veri Bütünlüğü ve Ölçeklenebilirlik

Geliştirdiğimiz yazılım uygulamaları, gün geçtikçe daha karmaşık hale geliyor ve kullanıcı beklentileri de sürekli yükseliyor. Özellikle kritik iş süreçlerini yöneten sistemlerde, sadece mevcut durumu değil, aynı zamanda bu duruma nasıl gelindiğinin de izlenebilir olması, veri bütünlüğü ve sistemlerin geleceğe dönük adapte olabilirliği büyük önem taşıyor. Geleneksel CRUD (Create, Read, Update, Delete) yaklaşımları, çoğu zaman bu tür derinlemesine ihtiyaçları karşılamakta yetersiz kalabiliyor.
Benim geliştirme tecrübelerimde, özellikle finans, lojistik veya e-ticaret gibi alanlarda, bir verinin neden değiştiğini, kim tarafından değiştiğini ve zaman içindeki tüm evrelerini kayıt altına almanın ne kadar kritik olduğunu defalarca gördüm. İşte bu noktada Event Sourcing ve onunla birlikte sıklıkla anılan CQRS (Command Query Responsibility Segregation) mimarileri devreye giriyor. Bu yazıda, Node.js'in asenkron ve olay tabanlı yapısıyla MongoDB'nin esnekliğinin birleşimiyle bu güçlü mimarileri nasıl hayata geçirebileceğinizi adım adım keşfedeceğiz. Amacımız, uygulamalarınızı daha izlenebilir, dayanıklı ve ölçeklenebilir hale getirmek.
Geleneksel CRUD Neden Yetersiz Kalabilir?
Çoğu geliştirici için CRUD, veritabanı etkileşimlerinin varsayılan modelidir. Bir kullanıcı profili güncellendiğinde, eski verinin üzerine yazılır. Ancak bu basitlik, beraberinde bazı dezavantajları da getirir:
- Geçmiş Kaybı: Bir verinin önceki durumu veya nasıl değiştiği hakkında hiçbir bilgi kalmaz.
- Denetlenebilirlik Eksikliği: Kimin, ne zaman ve neden bir değişikliği yaptığını takip etmek zorlaşır.
- Zorlu Hata Ayıklama: Uygulamanın bir noktada yanlış bir duruma düşmesi durumunda, hatanın kaynağını tespit etmek ve geri almak karmaşıktır.
- Ölçeklenebilirlik Sınırlamaları: Okuma ve yazma işlemlerinin aynı veritabanı modeline odaklanması, performans optimizasyonlarını zorlaştırabilir.
Bu sorunlar, özellikle finansal işlemler, sipariş yönetimi veya kullanıcı hareketleri gibi verinin geçmişinin önemli olduğu senaryolarda ciddi problemlere yol açabilir.

Event Sourcing Nedir ve Nasıl Çalışır?
Event Sourcing, uygulama durumundaki değişiklikleri sadece mevcut durumu güncelleyerek değil, bu değişiklikleri bir olay akışı (event stream) olarak kaydederek yöneten bir mimari yaklaşımdır. Geleneksel yaklaşımlar "şu anki durum nedir?" sorusuna odaklanırken, Event Sourcing "bu duruma nasıl gelindi?" sorusuna yanıt arar.
Temel Prensipler:
- Olaylar Gerçeğin Kaynağıdır: Sistemde meydana gelen her önemli değişiklik bir olay (event) olarak kaydedilir. Bu olaylar, geri döndürülemez ve sırasıyla depolanır. Örneğin, bir ürün oluşturulduğunda `ProductCreated` olayı, güncellendiğinde `ProductPriceUpdated` olayı kaydedilir.
- Durum Olaylardan Türetilir: Uygulamanın anlık durumu, depolanmış tüm olayların baştan sona oynatılması (replay edilmesi) ile elde edilir. Bu, bir banka hesabının bakiyesinin, tüm para yatırma ve çekme işlemlerinin toplamıyla bulunmasına benzer.
- Değişmezlik (Immutability): Olaylar bir kez depolandıktan sonra asla değiştirilemez veya silinemez. Bu, tam bir denetim izi ve veri bütünlüğü sağlar.
Event Sourcing'in Avantajları:
- Tam Denetim Kaydı: Her değişikliğin detaylı bir geçmişi tutulur.
- Zamanda Yolculuk (Time Travel): Uygulamanın herhangi bir zamandaki durumunu yeniden oluşturabilir veya hatalı olayları telafi edebilirsiniz.
- Hata Kurtarma ve Geri Alma: Yanlış olayları tespit edip telafi etme veya sistemi belirli bir noktaya geri döndürme yeteneği.
- İş Zekası ve Analitik: Olay akışları, iş süreçleri hakkında zengin analitik veriler sunar.
- Daha Esnek Mimari: Yeni okuma modelleri (projection'lar) mevcut olaylardan kolayca oluşturulabilir.
Event Sourcing'in Zorlukları:
- Karmaşıklık: Geleneksel CRUD'a göre daha karmaşıktır ve öğrenme eğrisi gerektirir.
- Hata Ayıklama: Dağıtık bir sistemde olay akışlarını izlemek ve hataları ayıklamak daha zor olabilir.
- Olay Şeması Değişiklikleri: Olay şemalarını değiştirmek (event versioning) özenli bir yönetim gerektirir.
CQRS Nedir ve Event Sourcing ile İlişkisi?
CQRS (Command Query Responsibility Segregation), bir sistemin okuma (Query) ve yazma (Command) işlemlerinin sorumluluklarını birbirinden ayıran bir mimari desendir. Bu ayrım, her iki operasyon türünün bağımsız olarak optimize edilmesini ve ölçeklenmesini sağlar.
CQRS'in Temel Prensipleri:
- Command (Yazma) Modeli: İşlemlerin yürütüldüğü ve uygulamanın durumunu değiştiren komutlar. Bu komutlar, Event Sourcing'de olaylar üretir.
- Query (Okuma) Modeli: Verilerin okunması ve sunulması için optimize edilmiş, genellikle sadeleştirilmiş veri modelleridir (projection'lar).
Event Sourcing ve CQRS Birlikte Nasıl Çalışır?
Event Sourcing, veri yazma tarafında (Command) harika bir çözüm sunar. Bir komut geldiğinde, bu komut işlenir ve bir veya daha fazla olay Event Store'a kaydedilir. Daha sonra, bu olaylar bir olay veri yolu (event bus) aracılığıyla abonelere (subscribers) yayınlanır. Bu aboneler genellikle okuma modellerini (Query) güncelleyen "projection" servisleridir. Örneğin, `OrderPlaced` olayı geldiğinde, bir projection servisi bu olayı alarak `CustomerOrders` koleksiyonunu güncelleyebilir.
Node.js ve MongoDB ile Event Sourcing & CQRS Uygulaması
Node.js'in olay tabanlı ve asenkron I/O modeli, Event Sourcing için adeta biçilmiş kaftan. Her işlem bir olay, her olay bir yayın ve her yayın bir dinleyici. Bu döngü, Node.js'in temel çalışma prensibiyle mükemmel uyum sağlar. Node.js Event Loop'a Derin Dalış yazımda bahsettiğim gibi, engellemeyen operasyonlar bu mimarinin verimli çalışmasını sağlar.
MongoDB ise esnek belge modeli sayesinde, olayları (event store) ve okuma modellerini (projection'lar) depolamak için ideal bir seçenektir. Olaylar, basit bir koleksiyonda BSON (JSON benzeri) belgeler olarak saklanabilirken, okuma modelleri de uygulamamızın sorgu ihtiyaçlarına göre farklı koleksiyonlarda, farklı şemalarda tutulabilir. Hatırlarsanız, MongoDB ile Node.js Uygulamalarında Veri Optimizasyonu yazımda indeksleme ve agregasyon tekniklerinin öneminden bahsetmiştim; bu teknikler, özellikle okuma modellerinin performansını artırmak için kritik olacaktır.
Mimari Bileşenler:
Command Handler: İstemciden gelen komutları (örn. `CreateUserCommand`) alır, iş mantığını uygular ve bir veya daha fazla olay üretir.
Event Store (MongoDB): Üretilen olayları kronolojik sırada depolayan merkezi bir veritabanı. Her olay, unique bir kimlik, olay tipi, veri yükü, zaman damgası ve olay sürümü içermelidir.
Event Bus/Publisher: Event Store'a kaydedilen olayları, ilgili tüm abonelere (projection'lar) yayınlayan mekanizma. Basit uygulamalar için Node.js `EventEmitter` yeterli olabilirken, dağıtık sistemler için Kafka, RabbitMQ veya Redis Pub/Sub gibi mesaj kuyrukları (Anlık Etkileşim: Node.js, WebSockets ve Socket.IO yazısındaki gibi) kullanılır.
Projection / Read Model (MongoDB): Event Bus'tan olayları dinler, bu olayları işler ve verileri istemciler için optimize edilmiş bir formata dönüştürerek ayrı bir koleksiyonda depolayan servisler. Örneğin, bir `UserCreated` olayı `UserReadModel` koleksiyonuna yeni bir kullanıcı ekler; `UserProfileUpdated` olayı mevcut kullanıcıyı günceller.
Query Handler: İstemciden gelen sorguları (örn. `GetUserByIdQuery`) alır ve Read Model'den veri çeker.
Örnek Uygulama Akışı:
// 1. Command (Yazma İşlemi) - Node.js Backend
class UserService {
constructor(eventStore, eventBus) {
this.eventStore = eventStore;
this.eventBus = eventBus;
}
async createUser(userId, username, email) {
// İş mantığı ve validasyonlar burada
if (!userId || !username || !email) {
throw new Error('Kullanıcı bilgileri eksik.');
}
const event = {
id: new Date().getTime(),
type: 'UserCreated',
payload: { userId, username, email },
timestamp: new Date().toISOString()
};
await this.eventStore.save(event); // Olayı kaydet
this.eventBus.publish(event); // Olayı yayınla
console.log(`Command: Kullanıcı oluşturuldu - ${username}`);
}
}
// 2. Event Store (MongoDB)
class MongoEventStore {
constructor(db) {
this.collection = db.collection('events');
}
async save(event) {
await this.collection.insertOne(event);
console.log(`Event Store: Olay kaydedildi - ${event.type}`);
}
async getEventsForUser(userId) {
return this.collection.find({ 'payload.userId': userId }).sort({ timestamp: 1 }).toArray();
}
}
// 3. Event Bus (Basit EventEmitter ile)
const EventEmitter = require('events');
class CustomEventBus extends EventEmitter {
publish(event) {
this.emit(event.type, event);
console.log(`Event Bus: Olay yayınlandı - ${event.type}`);
}
}
// 4. Projection / Read Model (MongoDB)
class UserProjectionService {
constructor(db, eventBus) {
this.userReadModel = db.collection('userReadModel');
eventBus.on('UserCreated', this.handleUserCreated.bind(this));
eventBus.on('UserProfileUpdated', this.handleUserProfileUpdated.bind(this));
console.log('Projection Service: Olay dinleyicileri ayarlandı.');
}
async handleUserCreated(event) {
const { userId, username, email } = event.payload;
await this.userReadModel.insertOne({ _id: userId, username, email, createdAt: event.timestamp });
console.log(`Read Model: Yeni kullanıcı kaydedildi - ${username}`);
}
async handleUserProfileUpdated(event) {
const { userId, newEmail } = event.payload;
await this.userReadModel.updateOne({ _id: userId }, { $set: { email: newEmail } });
console.log(`Read Model: Kullanıcı ${userId} güncellendi.`);
}
}
// MongoDB bağlantısı (örnek)
const { MongoClient } = require('mongodb');
const uri = 'mongodb://localhost:27017/event_sourcing_db';
async function main() {
const client = new MongoClient(uri);
await client.connect();
const db = client.db();
const eventStore = new MongoEventStore(db);
const eventBus = new CustomEventBus();
const userService = new UserService(eventStore, eventBus);
new UserProjectionService(db, eventBus); // Read Model'i başlat
// Komut gönder
await userService.createUser('user123', 'İsmail YAĞCI', 'ismailyagci371@gmail.com');
// Query (Okuma İşlemi)
const user = await db.collection('userReadModel').findOne({ _id: 'user123' });
console.log('Query: Kullanıcı bilgisi:', user);
await client.close();
}
main().catch(console.error);
Avantajları ve Dikkat Edilmesi Gerekenler
- Ölçeklenebilirlik: Okuma ve yazma modellerini bağımsız olarak ölçeklendirebilirsiniz. Okuma modelleri, genellikle okuma performansı için optimize edilmiş, hatta birden fazla kopyası olan denormalize edilmiş veriler içerebilir.
- Performans: Özellikle okuma yoğun uygulamalarda, özel olarak tasarlanmış Query modelleri sayesinde çok hızlı yanıt süreleri elde edilebilir.
- Mimari Esnekliği: Yeni iş gereksinimleri ortaya çıktığında, mevcut olay akışından yeni okuma modelleri oluşturmak mümkündür. Bu, gelecekteki değişikliklere karşı mimarinizi daha dirençli hale getirir.
- Kompleksite Yönetimi: İş mantığını Commands ve Events'e ayırarak, daha temiz ve yönetilebilir bir kod tabanı elde edersiniz. Bu, Temiz Mimari prensipleriyle de uyumludur.
Sonuç ve Gelecek
Event Sourcing ve CQRS mimarileri, modern ve karmaşık uygulamalar geliştirirken karşılaşılan birçok zorluğa güçlü çözümler sunar. Özellikle Node.js'in olay güdümlü doğası ve MongoDB'nin esnek veri modelleme yetenekleriyle birleştiğinde, bu yaklaşımlar uygulamalarınıza üstün bir veri bütünlüğü, izlenebilirlik ve ölçeklenebilirlik kazandırabilir.
Elbette, her mimari yaklaşımın kendine özgü bir öğrenme eğrisi ve ek karmaşıklığı vardır. Ancak, iş açısından kritik verilerin yönetimi, denetim ve gelecekteki genişleme ihtiyaçları söz konusu olduğunda, bu yatırımın karşılığını fazlasıyla alacağınızı söyleyebilirim. Unutmayın, bu mimariler özellikle yüksek performans, veri geçmişi ve iş zekası gerektiren sistemler için tasarlanmıştır. Her projenin kendi ihtiyaçlarına göre en uygun mimarinin seçilmesi önemlidir.
Eğer aklınıza takılan sorular olursa veya bu konularda daha derinlemesine bilgi almak isterseniz, bana ismailyagci371@gmail.com adresinden veya sosyal medya kanallarımdan ulaşabilirsiniz. Sağlıklı ve başarılı kodlamalar dilerim!
Yorumlar
Yorum Gönder