Clean Architecture + CQRS + Event Sourcing

⚙️ Clean Architecture + CQRS + Event Sourcing
Modern Uygulamaların Üç Kutsal Mimarisi
“Bir sistemin kalitesi, değişimi kaldırma kabiliyetiyle ölçülür.”
— Greg Young (CQRS & Event Sourcing’in yaratıcısı)
🧱 1️⃣ Clean Architecture Temeli: Zemin Oluşturma
Clean Architecture, bağımlılık akışını tanımlar:
Infrastructure → Application → Domain
Presentation → Application → DomainBu yapı bağımlılıkları dıştan içe yönlendirir.
Ancak kendi başına veri yönünü tanımlamaz.
İşte tam burada CQRS devreye girer.
🔀 2️⃣ CQRS: Komutlar ve Sorguların Ayrılığı
CQRS (Command and Query Responsibility Segregation) şunu söyler:
“Yazmak (Command) ile okumak (Query) farklı problemlerdir;
bu yüzden aynı modeli paylaşmamalıdırlar.”
📚 Geleneksel yaklaşım:
Service → Repository → Database⚙️ CQRS yaklaşımı:
Command → Aggregate (Domain) → Event Store / Outbox
Query → ReadModel → Database / CacheYani artık tek bir OrderService yoktur.
Onun yerine iki farklı akış vardır:
- Komut akışı: Davranışı değiştirir. (
CreateOrderCommand) - Sorgu akışı: Durumu okur. (
GetOrderByIdQuery)
🧩 3️⃣ Event Sourcing: Durumu Olaylarla Saklamak
Event Sourcing, sistemin durumunu klasik “tablolar” yerine geçmiş olayların toplamı olarak saklar.
“Event Store, sistemin zaman makinesidir.”
Örnek:
UserRegistered
UserProfileUpdated
UserEmailChangedBu event’leri sırayla uygularsan:
var user = new User();
user.Apply(new UserRegistered(...));
user.Apply(new UserProfileUpdated(...));
user.Apply(new UserEmailChanged(...));Sonuç: Kullanıcının güncel hali.
Ama aynı zamanda geçmişin tüm versiyonlarına erişebilirsin.
🧠 4️⃣ Üç Mimari Nasıl Birleşir?
| Katman | Clean Architecture Rolü | CQRS Rolü | Event Sourcing Rolü |
|---|---|---|---|
| Domain | Saf iş kuralları | Aggregates / Entities | Event’leri üretir ve uygular |
| Application | Use Case’leri yönetir | Command & Query Handler’lar | Event’leri Outbox’a yazar |
| Infrastructure | Teknik detayları barındırır | Read Model DB, Outbox, Message Bus | Event Store implementasyonu |
| Presentation | UI / API / Controller | Komut & Sorgu endpoint’leri | Kullanıcıyla etkileşim noktası |
Bu üç mimari aynı hedefe farklı yollardan gider:
- Clean Architecture: Bağımlılık yönünü temizler
- CQRS: Veri akışını ayırır
- Event Sourcing: Durumu izlenebilir hale getirir
Birlikte kullanıldığında:
“Değişim yönetilebilir, geçmiş izlenebilir, kod test edilebilir hale gelir.”
⚙️ 5️⃣ Gerçek Dünya Akışı
Aşağıda modern .NET sistemlerinde sıkça gördüğümüz tam akış modeli yer alıyor:
[API Controller]
↓
[Command Handler (Application)]
↓
[Aggregate (Domain)]
↓
[Domain Events Raised]
↓
[Outbox (Infrastructure)]
↓
[Event Bus → Kafka / Redis Stream]
↓
[Projection Handler → Read Database]
↓
[Query Handler (Application)]
↓
[Read Model Returned]Her adımın görevi açık, bağımlılıklar ters:
- Domain → event üretir
- Application → event’i kaydeder
- Infrastructure → event’i taşır
- Query → yeni durumu gösterir
💾 6️⃣ Outbox Pattern: Mimari Tutkal
Event Sourcing sistemlerinde en kritik problem,
“event’in kaydedilip yayınlanmama riski”dir.
Çözüm: Outbox Pattern
(Transactional Outbox olarak da bilinir.)
🧩 Ne işe yarar?
- Event, Domain işlemiyle aynı transaction’da Outbox tablosuna yazılır.
- Background worker (ör.
OutboxDispatcher) bu kaydı alır,
Kafka, RabbitMQ, Redis gibi sisteme gönderir. - Event başarılı şekilde yayıldığında Outbox’tan silinir.
📄 Örnek Tablo:
| Id | AggregateId | EventName | Payload | Published |
|---|---|---|---|---|
| 1 | 1234 | UserCreated | {JSON} | false |
📦 Uygulama
public class UserAggregate : AggregateRoot
{
public void Register(string email)
{
AddEvent(new UserRegisteredEvent(email));
}
}public class OutboxWriter : IOutboxWriter
{
public async Task SaveAsync(DomainEvent evt)
{
_context.Outbox.Add(new OutboxMessage(evt));
await _context.SaveChangesAsync();
}
}🧩 7️⃣ Projection (Read Model) Tarafı
Event Sourcing’in “okuma” tarafında klasik bir tablo yerine
projection bulunur: Event’lerin sonucu olarak oluşan,
okuma için optimize edilmiş yapı.
Örnek:
public class UserProjectionHandler :
IEventHandler<UserRegistered>,
IEventHandler<UserEmailChanged>
{
private readonly ReadDbContext _readDb;
public async Task Handle(UserRegistered e) =>
await _readDb.Users.AddAsync(new UserReadModel { Id = e.Id, Email = e.Email });
public async Task Handle(UserEmailChanged e)
{
var user = await _readDb.Users.FindAsync(e.Id);
user.Email = e.NewEmail;
await _readDb.SaveChangesAsync();
}
}Projection’lar idempotent olmalı, yani aynı event birden fazla gelse bile sonuç değişmemeli.
🔀 8️⃣ CQRS + Event Sourcing + Clean Architecture Entegrasyon Örneği
src/
├── Core/
│ ├── Domain/
│ │ ├── Aggregates/
│ │ │ └── QuizAggregate.cs
│ │ ├── Events/
│ │ │ └── QuizStartedEvent.cs
│ │ └── Shared/
│ │ └── Entity.cs
│ ├── Application/
│ │ ├── Commands/
│ │ │ └── StartQuizCommand.cs
│ │ ├── Queries/
│ │ │ └── GetQuizByIdQuery.cs
│ │ ├── Handlers/
│ │ │ ├── StartQuizCommandHandler.cs
│ │ │ └── GetQuizByIdQueryHandler.cs
│ │ └── Interfaces/
│ │ └── IQuizRepository.cs
├── Infrastructure/
│ ├── Persistence/
│ │ ├── EventStoreDbContext.cs
│ │ └── OutboxDispatcher.cs
│ ├── Messaging/
│ │ └── KafkaProducer.cs
│ └── Projections/
│ └── QuizProjectionHandler.cs
├── Presentation/
│ └── Controllers/
│ └── QuizController.cs
└── Shared/
└── BuildingBlocks/
├── Result.cs
└── IClock.csBu yapı:
- Clean Architecture’ın bağımlılık yönünü korur.
- CQRS’in “Command vs Query” ayrımını uygular.
- Event Sourcing ile sistemin geçmişini saklar.
- Outbox ile dağıtık güvenliği sağlar.
🧠 9️⃣ Gelişmiş Konseptler
| Kavram | Açıklama | Uygulama Katmanı |
|---|---|---|
| Snapshotting | Uzun geçmişi kısaltmak için ara durum kaydı | Infrastructure |
| Saga / Process Manager | Birden fazla Aggregate’i koordine eder | Application |
| Idempotency | Aynı event birden fazla işlense bile sonuç değişmez | Projection / Infrastructure |
| Event Versioning | Event şemalarının geriye dönük uyumluluğu | Domain / Serializer |
| Feature Flags | Strateji değişimini konfigürasyona taşır | Application |
📈 10️⃣ Ölçeklenebilirlik ve Performans
Bu üçlünün birleşimi sadece kod kalitesini değil,
sistem ölçeklenebilirliğini de doğrudan etkiler.
| Özellik | Faydası |
|---|---|
| Buffered Logging (IBufferedLogger) | Async loglama, düşük latency |
| Outbox + Retry mekanizması | Event kaybı olmadan asenkron yayılım |
| CQRS Query cache | Okuma performansını artırır |
| Snapshot’lar | Event replay süresini azaltır |
| Idempotent projection | Network partition durumlarında güvenlik |
🧩 11️⃣ Neden Bu Üçlü En Güçlü Kombinasyondur?
| Özellik | Clean Architecture | CQRS | Event Sourcing |
|---|---|---|---|
| Bağımlılık Kontrolü | ✅ | ❌ | ❌ |
| Veri Akışı Ayrımı | ❌ | ✅ | ✅ |
| Zaman Tabanlı Durum Yönetimi | ❌ | ❌ | ✅ |
| Test Edilebilirlik | ✅ | ✅ | ✅ |
| Değişime Dayanıklılık | ✅ | ✅ | ✅ |
| Auditability (Geçmiş Takibi) | ❌ | ❌ | ✅ |
Üçü bir araya geldiğinde:
“Test edilebilir, geçmişi izlenebilir, değişime dayanıklı bir sistem” ortaya çıkar.
🧠 12️⃣ Sonuç: Modern .NET Mimarisi = Clean + CQRS + Event Sourcing
Bu üçlü;
- Clean Architecture’ın bağımlılık izolasyonunu,
- CQRS’in veri akışı netliğini,
- Event Sourcing’in geçmiş izlenebilirliğini birleştirir.
Sonuç:
Zamanla bozulmayan, ölçeklenebilen, değişime uyumlu bir sistem.
🧩 13️⃣ TL;DR (Kısa Özet)
| Katman | Sorumluluk | Teknoloji |
|---|---|---|
| Domain | İş mantığı, Aggregate, Event üretimi | Pure C# |
| Application | Command/Query işleyiciler, Outbox yazımı | MediatR, FluentValidation |
| Infrastructure | Event Store, DB, Kafka, Redis, Projection | EF Core, Redis, Kafka |
| Presentation | API, Controller, DTO | ASP.NET Core, GraphQL |
“Event’ler geçmişi anlatır, CQRS akışı düzenler, Clean Architecture sınırları korur.”
— DotNet Senior Developer (C#)





