CQRS tek veritabanı üzerinde uygulanabilir. Command ve query sorumluluklarını ayırmak, veriyi mutlaka iki farklı sisteme kopyalamak anlamına gelmez. Yazma tarafı iş kurallarını ve değişiklikleri yönetirken okuma tarafı ihtiyaç duyulan DTO’yu üretir. Aynı tabloya bakmaları bu sorumluluk ayrımını ortadan kaldırmaz.
Bir ürünün yayınlanmasıyla liste ekranındaki gösterimini düşünelim. Command, ürünün yayınlanabilir durumda olup olmadığını değerlendirir. Query ise isim, kimlik ve yayın durumu alanlarını okur. Liste ekranına domain entity’sini açmak veya query için command modelini zorlamak gerekmez.
CQRS tek veritabanı ile hangi sorunu çözer?
Bir ürün yönetim ekranında güncelleme ve listeleme aynı entity üzerinden yürütülüyor olsun. Ekranda ürün adı, kategori adı, stok özeti ve son değişiklik zamanı gösteriliyor. Güncelleme işlemi ise yayın koşullarını ve kullanıcı yetkisini kontrol etmek zorunda. Entity’nin bütün alanlarını hem request hem response olarak kullanmak iki ayrı ihtiyacı birbirine bağlayabilir. Liste ekranına yeni özet alanı eklemek, business modelini değiştirmeyi gerektirebilir. Aynı şekilde yazma için kullanılan navigation property’ler, basit listede gereksiz veri yüklenmesine yol açabilir. CQRS burada sorumluluk ayrımı sağlar.
Bu ayrımı yapmak için fiziksel veri kopyası üretmek zorunda değiliz. Query doğrudan aynı tablodan ihtiyacı olan DTO’yu çıkarabilir. Command, veriyi değiştirirken iş kurallarını uygulayabilir. Aradaki fark, iki farklı connection string bulunması değildir. Veriyi değiştiren karar ile veriyi gösteren sözleşmenin farklı biçimde tasarlanmasıdır. Bu nedenle tek database kullanan bir uygulamada da yazma modelini domain davranışına, okuma modelini ekrana uygun hale getirmek mümkündür. Aynı schema kullanımı, model ayrımının değerini otomatik olarak ortadan kaldırmaz.
Öte yandan her CRUD işlemini command ve query sınıflarına ayırmak somut fayda üretmeyebilir. Sadece bir alanı okuyup yazan küçük bir yönetim ekranında handler sayısı artar, fakat iş davranışı farklılaşmayabilir. Kararı “CQRS kullanıyor muyuz?” sorusuna indirgemek yerine hangi sorumlulukların bağımsız değiştiğine bakın. Karmaşık güncelleme kuralları ile farklı read projection’lar varsa ayrım daha güçlü gerekçe taşır. Aynı şekli iki dosyada tekrar etmek ise maliyet yaratır. Pattern adı seçildikten sonra bütün feature’lara zorunlu şema dağıtmak gereksiz karmaşıklık oluşturabilir.
Tercihim: Önce aynı database üzerinde kullanım senaryosu bazında command ve query ayrımını kurmak. Böylece yeni bir mesajlaşma veya veri senkronizasyon sistemi eklemeden model farklılığını değerlendirebiliriz. Bu seçimin bedeli, database kaynaklarının hâlâ ortak olmasıdır. Query yükü yazma işlemini etkileyebilir ve fiziksel ölçekleme aynı sistemin koşullarına bağlı kalır. Ayrı read store’a geçme kararı, gözlenen query ihtiyaçları ve operasyon kapasitesiyle değişir. Ayrımın ilk adımı fiziksel depolama olmak zorunda değildir; ama fiziksel bağımlılığın sürdüğü de açıkça kabul edilmelidir.
Command’ı property değiştirme isteğinden ayırmak
Ürünün Published alanını true yapmak teknik bir güncellemedir. “Ürünü yayınla” ise bir iş niyetidir. İş niyeti, ürün adının bulunması, fiyatın geçerli olması veya gerekli onayın alınması gibi kurallar taşıyabilir. Command bu niyeti temsil ettiğinde istemcinin entity’nin bütün alanlarını bilmesi gerekmez. API’ye yalnızca değiştirilebilir property’leri açmak ile anlamlı iş işlemi sunmak farklı seçeneklerdir. Hangi seçimin doğru olduğu uygulamanın kurallarına bağlıdır. İşlemi bir command sınıfına taşımak, business niyetini kendiliğinden açık hale getirmez.
Örnekte yayın için ürün adının boş olmaması kontrol edilir. Bu kural sınırlı bir öğretim senaryosudur; bütün katalog gereksinimlerini kapsamaz. Gerçek bir uygulamada yayın sonrası isim değişikliği, fiyat güncellemesi ve geri çekme gibi işlemler de mevcut durumu etkileyebilir. Bu kuralların hangi katmanda tutulacağı belirlenmelidir. Birden fazla command aynı invariant’ı koruyorsa kuralı handler’larda tekrar etmek yerine domain davranışına taşımak anlamlı olabilir. Command handler çoğunlukla yükleme, yetki bağlamı, orchestration ve commit’i yönetir. Domain kuralını taşıyan tipin persistence ayrıntısını bilmesi gerekmeyebilir.
Command’ın aldığı kimlik, kullanıcının o kaydı değiştirmeye yetkili olduğunu kanıtlamaz. Yayınlama işlemi için kimlik doğrulama ve uygun yetki gerekir. Tenant kullanılan sistemde ürünün çağıran tenant’a ait olması ayrıca kontrol edilmelidir. Query tarafında görülebilen bir ürün, command tarafından değiştirilebilir olmak zorunda değildir. Bu farkı DTO’nun varlığıyla çözemezsiniz. Yetkilendirme kullanım senaryosuna uygun yapılmalıdır. Bu makaledeki console örneğinde kullanıcı bağlamı bulunmadığından böyle bir güvenlik garantisi uygulanmamıştır; API’ye taşınırken açık biçimde eklenmesi gerekir.
Broker olmadan command ve query
Aşağıdaki .NET 10 console örneği, EF Core SQLite 10.0.12 ile çalışır. Console projesine Microsoft.EntityFrameworkCore.Sqlite paketini bu sürümle ekleyin. Buradaki handler’lar normal sınıflardır; CQRS için MediatR, event sourcing veya broker zorunlu değildir.
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
await using var connection = new SqliteConnection(
"Data Source=:memory:"
);
await connection.OpenAsync();
var options = new DbContextOptionsBuilder<CatalogDb>()
.UseSqlite(connection)
.Options;
await using (var write = new CatalogDb(options))
{
await write.Database.EnsureCreatedAsync();
write.Products.Add(new Product { Name = "Keyboard" });
await write.SaveChangesAsync();
await new PublishProduct(write).HandleAsync(
1,
CancellationToken.None
);
}
await using var read = new CatalogDb(options);
var dto = await read
.Products.AsNoTracking()
.Where(x => x.Id == 1)
.Select(x => new ProductDto(x.Id, x.Name, x.Published))
.SingleAsync();
if (!dto.Published)
throw new InvalidOperationException("Published value missing.");
Console.WriteLine(dto);
public sealed record ProductDto(int Id, string Name, bool Published);
public sealed class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public bool Published { get; set; }
}
public sealed class CatalogDb(DbContextOptions<CatalogDb> options)
: DbContext(options)
{
public DbSet<Product> Products => Set<Product>();
}
public sealed class PublishProduct(CatalogDb db)
{
public async Task HandleAsync(int id, CancellationToken ct)
{
var product =
await db.Products.SingleOrDefaultAsync(x => x.Id == id, ct)
?? throw new KeyNotFoundException("Product missing.");
if (string.IsNullOrWhiteSpace(product.Name))
throw new InvalidOperationException("Name required.");
product.Published = true;
await db.SaveChangesAsync(ct);
}
}
Command context üzerinde değişiklik yapıp kaydeder. Query farklı bir context ile aynı database’den DTO okur. Bu örnekte commit tamamlandıktan sonra okuma yapılır. Read replica, cache veya ayrı projection kullanılmadığı için ayrıca mesajla projection güncelleme süreci yoktur.
Ayırmanın kazancı ve bedeli
Command tarafında transaction ve validation kararlarını, query tarafında projection ve sayfalama kararlarını ayrı geliştirebiliriz. Ancak iki klasör açıp aynı generic service’i çağırmak tek başına bu ayrımı kurmaz. Yazma işleminin anlamı ve okumanın contract’ı farklı ihtiyaçlara cevap vermelidir.
Basit CRUD ekranlarında ayrı handler sayısını artırmanın değeri sınırlı olabilir. Tercihim, davranış ve okuma gereksinimleri farklılaşınca bu ayrımı yapmak. Her GET ve POST için otomatik sınıf üretmek yerine, değişim nedenlerini görünür tutarım. CQRS değerlendirmesini okurken veri ayrımını da bu bağlamda düşünün.
Ayrı read store ne zaman anlamlı olur?
Arama, analitik veya farklı bir sorgu modelinin aynı database üzerinde karşılanması zorlaşabilir. Böyle bir ihtiyaç ayrı read store’u gerekçelendirebilir. Bu durumda güncelleme gecikmesi, yeniden kurma, duplicate event ve projection version yönetimi tasarıma girer. “Yazdım, hemen her ekranda aynı değeri görürüm” beklentisi ayrıca ele alınmalıdır.
Microsoft’un CQRS rehberi aynı veri deposuyla temel ayrımı ve ayrı depolarla daha gelişmiş modeli ayırıyor. Performans kazancı otomatik değildir; workload, sorgu planı ve operasyon maliyeti ölçülmelidir.
Yetkilendirme de iki tarafta korunmalıdır. Query’nin DTO üretmesi kullanıcıya bütün ürünleri görme yetkisi vermez. Tenant veya sahiplik filtresi, command kadar read tarafında da uygulanır. Bu demo authentication içermez; gerçek API’ye taşırken sorgu kapsamını authenticated kullanıcıdan üretin.
İlk aşamada aynı database ile ilerlemek, eventual consistency sorunlarını gereksiz yere eklememeyi sağlar. Sorumluluklar ayrıldıktan sonra depolama ayrımını ihtiyaçla gerekçelendirmek daha kontrollü bir adımdır.
Query modelini ekranın ihtiyacına göre şekillendirmek
Ürün listesinde isim, kategori ve yayın durumunu görmek yeterliyse query bu alanları üretmelidir. Domain entity’sinin bütün ilişkilerini taşımaya gerek olmayabilir. Böylece response contract, persistence modelindeki yeni alanlardan daha az etkilenir. Bu fayda için ayrı read database zorunlu değildir; projection aynı relational tablolardan hazırlanabilir. Query sahibi, sıralama, filtreleme ve sayfalama davranışını belirler. Bu kararlar sadece teknik kolaylık olarak görülmemelidir. Kullanıcıların gördüğü sonuçların kapsamı, özellikle erişim politikaları ve değişen veri altında kararlılık açısından önemlidir.
Read model’e bütün business logic’i yasaklayan mekanik bir kural da dikkatle yorumlanmalıdır. Query veriyi değiştirmemeli ve write invariant’larını yönetmemelidir. Fakat kullanıcıya gösterilen bir etiketin türetilmesi, biçimlendirme veya erişim kapsamının belirlenmesi gibi kararlar bulunabilir. Bunların hangi sistemin otoritatif iş kararı olduğunu ayırmak gerekir. Liste ekranındaki “stok yeterli görünüyor” bilgisi, sipariş oluşturma sırasında gerçek rezervasyon kontrolünün yerine geçemez. Query’nin amacı kullanıcıyı bilgilendirmek olabilir; command’ın amacı tutarlı değişiklik yapmak olur. Aradaki fark, read model güncel olsa bile korunmalıdır.
Kod örneğinde context’ler neden ayrılıyor?
Önce yazma context’i schema ve ürünü oluşturuyor. Ardından PublishProduct handler’ı ürünü kimlikle buluyor, adını kontrol ediyor ve yayın durumunu değiştirip kaydediyor. Yazma context’i dispose olduktan sonra başka context üzerinden DTO okunuyor. Böylece query sonucu, önceki context’in bellekte takip ettiği entity’ye bakarak değil database’den üretiliyor. Bu düzen öğretim açısından command commit’i ile query okumasını belirginleştiriyor. Aynı connection kullanılması SQLite in-memory database’in korunması içindir. İki context’in bulunması, iki farklı database olduğu anlamına gelmez.
Örnekte okuma, commit tamamlandıktan sonra yapılıyor. Arada asynchronous projection güncellemesi, cache veya replica bulunmuyor. Bu nedenle ayrı read store’un gecikme ve yeniden kurma problemleri uygulanmış değildir. dto.Published kontrolü bu çalıştırmada commit edilen değişikliğin sonraki query’de görünmesini doğrular. Bu sonuç farklı isolation seviyeleri, farklı provider’lar veya paralel işlemler için kapsamlı garanti vermez. Özellikle production’da uzun transaction içinde query yapan bir tüketicinin görünürlük beklentisi başka olabilir. Demo’nun sıralı akışından bütün sistemin tutarlılık davranışını çıkarmamalıyız.
Handler normal bir sınıftır ve HandleAsync doğrudan çağrılır. Böylece CQRS’nin bir mediator kütüphanesine bağlı olmadığını gösterir. Bir library kullanmak routing, pipeline veya cross-cutting davranış için faydalı olabilir; fakat sorumluluk ayrımının kendisi kütüphaneden gelmez. Aynı handler içindeki generic service bütün read ve write işlemlerini ortak entity modeliyle yürütüyorsa mediator eklenmiş olması tasarım farkını kurmaz. Araç seçimi ile mimari kararın gerekçesini ayrı tutmak, dependency ve lisans maliyetini de daha açık değerlendirmeyi sağlar.
Commit sonrasında istemci ne beklemeli?
Kullanıcı yayınla düğmesine bastığında uygulama ürünün hemen listede görünmesini bekleyebilir. Aynı database’den, commit sonrasında yapılan query bu beklentiyi karşılayabilir; ancak cache kullanılıyorsa eski sonuç gösterilebilir. Bu durumda problem fiziksel CQRS ayrımı olmadığı halde oluşur. Yazma işlemi tamamlandıktan sonra istemciye işlem sonucu veya yeni veri biçimi döndürmek bazı ekranlarda daha açık olabilir. Yine de response’a eklenen alanların hangi kaynaktan geldiği ve commit öncesi mi sonrası mı üretildiği belirlenmelidir. Kullanıcıya gösterilen başarı işin gerçek durumu ile uyumlu olmalıdır.
Asynchronous command processing seçildiğinde kabul edildi ile tamamlandı farklıdır. Kuyruğa alınmış isteğin 202 yanıtı, ürünün yayınlanmış olduğunu göstermez. İstemci işlem durumunu sorgulayabilir veya tamamlanma bildirimini takip edebilir. Bu düzen bütün CQRS uygulamalarında gerekli değildir; özel bir işlem modeli seçimidir. Kullanıcı arayüzü bu farkı anlamazsa listede görünmeyen ürünü tekrar yayınlamaya çalışabilir. Tekrarın iş etkisini önlemek için command kimliği veya uygun idempotency davranışı gerekebilir. Pattern adından istemci etkileşim akışını otomatik türetmek doğru değildir.
Concurrency sorununu model ayrımı çözmez
İki yönetici aynı ürünü farklı amaçla değiştirdiğinde command ve query sınıflarının ayrı olması lost update riskini kaldırmaz. Command, kullanıcının gördüğü sürümle mevcut kayıt arasında çatışma olup olmadığını değerlendirebilir. Uygun concurrency token ve provider davranışı gereklidir. Çatışma ortaya çıktığında işlemi otomatik tekrar etmek her business kuralı için doğru olmayabilir. Kullanıcı yeni durumu görüp kararını yenilemek zorunda kalabilir. Bu karar, command’ın niyeti ve değişen alanlarla ilgilidir. Mimari ayrım ancak bu sorumluluğun doğru yerde görünmesine yardımcı olur.
Örnekte yayın durumunu true yapmanın tekrar edilmesi sonuç değerini değiştirmese de gerçek yayın işlemi harici indeksleme veya bildirim üretebilir. Böyle yan etkiler eklenirse tekrar çalıştırmanın anlamı değişir. State assignment’ın idempotent görünmesi, bütün işlemin idempotent olduğunu kanıtlamaz. Command contract’ında başarının ne olduğu, mevcut yayın durumunda ne yapılacağı ve yan etkinin nasıl koordine edileceği belirlenmelidir. Aynı database yaklaşımı yerel transaction’ı kolaylaştırabilir; harici sistemleri aynı garantiye dahil etmez. Bu ayrımı başlangıçta yazmak ileride yanlış retry tasarımını önler.
Ayrı read store için önce darboğazı bulmak
Listeleme yavaşsa ilk adım yeni database eklemek olmamalıdır. Gereksiz kolonlar, geniş Include kullanımı, index eksikliği veya kontrolsüz sonuç miktarı incelenmelidir. Query planı ve gerçek veri dağılımı görülmeden darboğazın storage mimarisi olduğu söylenemez. Aynı database üzerinde projection ve uygun pagination ile ihtiyaç karşılanabilir. Ayrıca uygulama ile database arasındaki gecikme veya istemcide işlenen büyük payload da toplam süreyi etkiler. Ölçüm, sorgunun nerede zaman ve kaynak tükettiğini göstermelidir. CQRS tek veritabanı yaklaşımı bu düzeltmeleri yapmaya engel değildir.
Arama ihtiyaçları relational modelden farklılaşmışsa ayrı bir index veya read store anlamlı olabilir. Örneğin çok farklı ekranlar aynı karmaşık join ve hesaplamaları tekrar ediyorsa önceden hazırlanmış model değerlendirilebilir. Bunun bedeli güncelleme süreci, veri tutarlılığı ve yeniden kurma operasyonudur. Yeni store’a geçiş için beklenen fayda ve kabul edilen gecikme açık yazılmalıdır. Ayrı sistem kurmak yalnızca query kodunu kısaltmaz; ek yedekleme, kapasite ve hata yönetimi sorumluluğu yaratır. Bu maliyeti yönetebilecek ekip düzeni de kararın parçasıdır.
Eventual consistency kullanıcı davranışını etkiler
Yazma ile projection güncellemesi arasında gecikme varsa kullanıcı tamamlanmış işlemi listede henüz göremeyebilir. Bu durumda geçici durum göstermek veya command sonucu üzerinden ekranı güncellemek gerekebilir. Ancak iyimser arayüz gösterimi başarısız işlemi başarılı gibi göstermemelidir. Read model’in hangi sürüme kadar güncel olduğu bilgisi bazı senaryolarda yararlı olabilir. Kabul edilebilir gecikme yalnızca saniye cinsinden teknik hedef değildir; kullanıcının hangi iş kararını verebildiğiyle ilgilidir. Finansal veya stokla ilgili kritik kararları eski projection üzerinden kesinleştirmek uygun olmayabilir.
Mesajın tekrar gelmesi projection toplamını iki kez artırabilir. Mesajların farklı sırada gelmesi eski durumun yeniyi ezmesine yol açabilir. Bu nedenle her projection update’i yalnızca consumer metodunu yazmakla tamamlanmaz. Kimlik, sıra veya sürüm bilgisi ve işleme politikası değerlendirilmelidir. Bu makalede ayrı store uygulanmadığı için bu davranışları çalıştırdığımızı iddia etmiyoruz. Geçiş yapıldığında duplicate, out-of-order ve yeniden işleme testleri ayrı doğrulama alanı olur. Aynı database ile başlayan tasarım bu problemleri henüz sisteme eklemeyerek öğrenme alanını daha dar tutabilir.
Projection’ı yeniden kurmak nasıl düşünülmeli?
Read store kaybedildiğinde write database’den tekrar oluşturulabilecek mi? Eğer projection yalnızca güncel tablolardan türetilebiliyorsa tam tarama veya kontrollü batch işlem tasarlanabilir. Geçmiş olay sırası gerekiyorsa mevcut verinin yeterli olup olmadığı ayrıca incelenmelidir. CQRS kullanmak event history bulunduğunu garanti etmez. Event Sourcing ayrı bir karar olduğu için veri yeniden kurma ihtiyacını pattern adına emanet etmeyin. Projection’ın hangi kaynağı otorite kabul ettiği ve yeniden oluşturma sırasında canlı güncellemelerin nasıl ele alınacağı açıklanmalıdır. Aksi halde kurtarma planı yalnızca teorik kalır.
Yeni projection sürümü mevcut ekranı değiştirecekse eski ve yeni modeli bir süre birlikte yönetmek gerekebilir. Bu süreçte veri iki farklı biçimde hazırlanabilir ve geçiş koşulu açık seçilir. Projection tamamen hazır olmadan trafiği yeni store’a taşımak eksik sonuç üretir. Hazırlık kontrolü yalnızca toplam kayıt sayısı olmamalıdır; kritik alanların ve filtrelerin de doğruluğu sınanmalıdır. Geri dönüş gerektiğinde write tarafındaki değişikliklerin eski okuma modeliyle hâlâ uyumlu olması gerekir. Bu çalışma ayrı read store’un bakım maliyetini somutlaştırır.
Testleri model sınırına göre seçmek
Command testinde olmayan ürün, boş ad ve başarılı yayınlama ayrı durumlar olmalıdır. İş invariant’ı saf modeldeyse hızlı testlerle sınanabilir. Commit davranışı için gerçek persistence testi gerekir. Query testinde yalnızca kayıt sayısını değil projection alanlarını, filtreyi ve kararlı sıralamayı kontrol edin. Bir command’ın başarılı olması query’nin doğru DTO ürettiğini göstermez. Bu makaledeki örnek başarılı yayınlama sonrası DTO görünürlüğünü kontrol eder; bütün hata yollarını veya erişim politikalarını kapsamıyor. Yeni davranışlar eklendiğinde bu sınırlar doğrulama planında genişletilmelidir.
Ayrı read store eklendiğinde testlerin önemli bir kısmı senkronizasyon davranışına kayar. Commit sonrası mesaj üretimi başarısız olduğunda ne olur, consumer yeniden başladığında hangi kayıtlar tekrar gelir, aynı olay iki kez işlendiğinde projection nasıl kalır? Bu sorular yerel query testiyle yanıtlanamaz. Operasyon sinyalleri de güncelleme gecikmesi ve başarısız projection sayısını göstermelidir. Pattern’in faydası ancak doğru davranışla birlikte korunursa anlamlıdır. Test sayısını artırmak yerine yeni mimari sınırın hangi başarısızlıkları getirdiğini listeleyip onlara odaklanmak gerekir.
Cache ve read replica eklerken değişen garantiler
Tek database adı altında farklı okuma yolları bulunabilir. Query cache’den yanıt veriyorsa commit sonrası database’deki yeni değer ekranda hemen görünmeyebilir. Read replica kullanılıyorsa replication gecikmesi ayrıca değerlendirilir. Bu durumlarda command ve query modelinin ayrılığından bağımsız olarak görünürlük beklentisi değişmiştir. Kritik bir işlem sonrasında ana database’den okuma, cache invalidation veya istemciye işlem sonucu döndürme gibi seçenekler düşünülebilir. Seçim aynı sorgunun bütün tüketicileri için uygun olmayabilir. Hangi ekranda hangi güncelliğin gerektiğini açıkça belirtin.
Cache anahtarının tenant, filtre ve authorization bağlamını doğru kapsaması gerekir. Sadece ürün kimliğine göre saklanan response, farklı kullanıcıya yanlış veri sunabilir. Model ayrımı güvenlik politikasını unutturacak bir katman oluşturmamalıdır. Cache miss ve invalidation hataları için fallback davranışı da tasarlanmalıdır. Cache kullanılmadan önce ölçümle ihtiyaç gösterilmesi, operasyon alanını gereksiz büyütmeyi önler. Eklenen mekanizmanın toplam latency ve database yüküne etkisini gerçek workload üzerinde değerlendirin; kod örneğinin hızlı görünmesini performance kanıtı kabul etmeyin.
Bu kararlar mimari kayıtta tutarlılık başlığı altında yer alabilir. Ana database’den commit sonrası okuma, cache’den eventual güncellik ve replica okuması farklı kullanıcı beklentileri üretir. Hepsinin aynı DTO’yu döndürmesi aynı garantiyi taşıdıkları anlamına gelmez. Kullanıcının yeniden deneme davranışı ve kritik business kararları bu farklara göre ele alınmalıdır.
Aynı database’den kontrollü büyümek
Başlangıç kararını kısa bir mimari kayıtta belgeleyin: Hangi command’lar iş niyeti taşıyor, query’ler hangi DTO’ları üretiyor, transaction nerede tamamlanıyor ve kullanıcı hangi tutarlılığı bekliyor? Ardından ayrı read store’a geçiş için somut gerekçeleri belirtin. Örneğin ölçülmüş query darboğazı veya farklı arama modeli ihtiyacı bu kararı tetikleyebilir. Yalnızca CQRS kullanıldığı için ikinci database eklenmemelidir. Ortak storage’ın faydası düşük operasyon yüküdür; maliyeti ortak kaynak ve schema bağımlılığıdır. İkisini birlikte kabul etmek bilinçli seçim yapmayı sağlar.
Review sırasında iki klasörün bulunmasından çok davranışın yerine bakın. Command gerçekten değişiklik kararını yönetiyor mu? Query write model’i istemciye açmadan gerekli veriyi üretiyor mu? Authorization iki tarafta uygulanıyor mu? Commit ile istemci yanıtı arasında beklenti net mi? Bu sorulara açık yanıt verilebiliyorsa CQRS tek veritabanı düzeni anlamlı bir aşama olabilir. Fiziksel ayrım daha sonra gerekçelendirilebilir. İhtiyaç büyüdüğünde hangi garantilerin değişeceğini bilmek, başlangıçta bütün dağıtık sistem bileşenlerini kurmaktan daha kontrollü bir geliştirme yolu sunar.
