EventStorming ile Domain'i Keşfetmek: Big Picture'dan Aggregate'lere
EventStorming, domain'de olan biteni geçmiş zamanlı turuncu sticky'lerle bir timeline'a dizerek domain expert ile developer'ları aynı dilde buluşturan bir workshop formatıdır. Bookstore'un sipariş akışını Big Picture, Process Modeling ve Software Design seviyelerinden geçirip hot spot'ları, pivotal event'leri, policy'leri ve duvardan çıkan aggregate adaylarını koda döküyoruz.
Sürüm notu: Bu yazıdaki kodlar C# 14 ve .NET 10 ile derlenip denenmiştir.
Önce Tahmin Et
Bookstore ekibi bir öğleden sonrayı toplantı odasında geçiriyor. Üç developer'ın yanında müşteri hizmetlerinden Grace, depodan Linus ve finanstan Ada var. Duvarda sekiz metrelik bir kâğıt rulo, herkesin elinde turuncu yapışkan notlar. Kural tek: "sipariş sürecinde olan bir şeyi geçmiş zamanla yaz". Yirmi dakika sonra duvarda, karışık sırada, şu yedi not duruyor:
PaymentRefunded OrderShipped OrderPlaced ReturnRequested
StockReserved OrderDelivered PaymentCaptured
İki soru. Bu event'leri timeline üzerinde hangi sırayla dizersin? Ve sıralarken odadaki iki kişinin kesin olarak tartışacağı tek nokta hangisi? İpucu: Ada'ya göre para önce çekilmeli, Linus'a göre rafta olmayan kitabın parası çekilmemeli.
EventStorming Nedir?
EventStorming, Alberto Brandolini'nin 2013'te "Introducing Event Storming" başlıklı blog yazısıyla tanıttığı bir workshop formatı. Fikir basit: bir iş sürecini anlamak için önce veri modelini, ekranları ya da tabloları değil, domain'de olan biten şeyleri konuşursun. Her "olan şey" turuncu bir sticky'ye geçmiş zamanla yazılır (domain event) ve soldan sağa akan bir timeline'a yapıştırılır. Brandolini yöntemi zamanla "Introducing EventStorming" adlı kitapta toplamaya başladı; kitap Leanpub'da yayınlanıyor.
Neden event'lerle başlıyoruz? Çünkü domain expert'in dili event'lerdir. Linus "stok tablosunda Reserved kolonu artar" demez, "kitap ayrıldı" der. Ada "Payments tablosuna satır eklenir" demez, "ödeme alındı" der. Geçmiş zaman da bilinçli bir seçim: "ödeme al" bir niyet, "ödeme alındı" bir gerçek. Niyetler tartışılır, gerçekler sıraya dizilir. Serinin ikinci yazısında konuştuğumuz ubiquitous language'in ham maddesi bu duvarda ortaya çıkar.
İkinci önemli fikir, modelleme alanının sınırsız olması. Beyaz tahta dolar, slayt kayar, ama bir kâğıt rulo uzar. Oda da önemli: sandalyeler kenara çekilir, herkes ayakta ve duvarın önündedir. DDD (Domain-Driven Design) literatüründe "domain expert ile birlikte çalış" tavsiyesi çok söylenir, az uygulanır; EventStorming bu tavsiyeyi bir öğleden sonralık somut bir etkinliğe çevirir.
Üç Seviye: Big Picture, Process Modeling, Software Design
Aynı notasyon üç farklı amaçla kullanılır. DDD Crew'un açık kaynak "EventStorming Glossary & Cheat Sheet"i bu üç formatı şöyle ayırır:
| Seviye | Soru | Kim katılır | Çıktı |
|---|---|---|---|
| Big Picture | Bu iş bir bütün olarak nasıl işliyor, nerede acıyor? | 10–30 kişi: tüm departmanlar, developer'lar | Timeline, hot spot'lar, pivotal event'ler, bounded context adayları |
| Process Modeling | Bu süreç adım adım nasıl yürüyor, kim neye bakıp karar veriyor? | Sürecin sahipleri ve onu yazacak ekip | Command, policy, read model ve actor'lerle tutarlı bir akış |
| Software Design | Bu akışı hangi tasarımla, hangi kurallarla koda dökeceğiz? | Bounded context'in ekibi ve domain expert | Aggregate (constraint) adayları, command/event sözleşmeleri |
Sıra önemli. Big Picture'da kimse "bunu hangi class'a koyarız" diye sormaz; sorduğu an toplantı developer'ların toplantısına döner ve Grace susar. Process Modeling'de akışı netleştirirsin. Software Design'da ise artık tek bir bounded context'in içindesin ve hangi kuralın nerede korunacağını konuşursun. Bu yazıda Bookstore'un sipariş akışını üç seviyeden geçireceğiz.
Renk Sözlüğü
EventStorming'in gücü notasyonunun azlığında. Herkes beş dakikada öğrenir, kimsenin UML (Unified Modeling Language) bilmesi gerekmez. Bookstore oturumunda kullandığımız sözlük:
- Turuncu, domain event:
OrderPlaced,StockReserved. Geçmiş zaman, domain dilinde. - Mavi, command:
PlaceOrder,ReserveStock. Bir actor'ün ya da bir policy'nin niyeti; event'in hemen solunda durur. - Küçük sarı, actor: Müşteri, depo görevlisi. Command'i kim veriyor?
- Mor, policy: "
OrderPlacedolunca stok ayır". Bir event'i bir command'e bağlayan "her X olduğunda Y yap" kuralı; bazen bir insan, bazen bir otomasyon. - Yeşil, read model: "Siparişlerim" ekranı, depodaki toplama listesi. Actor'ün karar verirken baktığı bilgi.
- Geniş pembe, dış sistem: ödeme sağlayıcısı, kargo firmasının API'si (Application Programming Interface).
- Büyük açık sarı, aggregate: kuralları koruyan ve command'i kabul ya da reddeden şey. DDD Crew bu not için artık "constraint" adını kullanıyor; Big Picture'da domain expert'e "aggregate" demek kafa karıştırıyor, "burada hangi kural var" demek karıştırmıyor.
- Kırmızı ya da neon pembe, hot spot: soru, çelişki, acı noktası. Çözülmez, işaretlenir.
Bookstore Oturumu: Big Picture
Oturum, Brandolini'nin tarif ettiği adımlarla ilerliyor. İlk yirmi dakika "chaotic exploration": herkes kendi bildiği event'leri sessizce yazıp duvara yapıştırıyor. Aynı şey üç farklı isimle yazılıyor ("sipariş alındı", "sipariş oluşturuldu", "checkout tamamlandı"); bu bir sorun değil, tam tersine ubiquitous language tartışmasının başlangıcı. Sonra timeline netleştiriliyor (Brandolini'nin deyişiyle "enforce the timeline"): event'ler soldan sağa sıraya giriyor, kopyalar birleşiyor, isimler üzerinde anlaşılıyor. Ekip, serinin glossary'sindeki OrderPlaced isminde uzlaşıyor.
Açılıştaki sorunun cevabı burada çıkıyor. Mutlu yol:
OrderPlaced → StockReserved → PaymentCaptured → OrderShipped → OrderDelivered
→ ReturnRequested → PaymentRefunded
Tartışma StockReserved ile PaymentCaptured arasında. Ada parayı hemen çekmek istiyor, çünkü kampanya döneminde ödeme sağlayıcısının cevap süresi uzuyor. Linus ise geçen yıl sonu kampanyasında olmayan kitapların parasının çekildiğini ve iadelerin iki hafta sürdüğünü anlatıyor. Kimse haklı ya da haksız değil; bu bir hot spot. Oturumu yöneten facilitator bunu çözmeye çalışmaz, kırmızı bir not yapıştırır ve devam eder. Duvarda birikip konuşulan diğer hot spot'lar: "Stok yetmezse sipariş ne olur?", "Ödeme sağlayıcısı 5 saniyede cevap vermezse?" ve "İade kargosunu kim öder?".

Sonra pivotal event'ler seçiliyor: sürecin bir aşamadan diğerine geçtiği, sahipliğin değiştiği birkaç kritik event. Bookstore'da bunlar OrderPlaced (sipariş ekibinden depoya), PaymentCaptured (finanstan sevkiyata), OrderShipped (bizden kargo firmasına) ve ReturnRequested. Bu event'lerin arasına dikey çizgiler çekince duvar kendiliğinden bölgelere ayrılıyor ve her bölgenin kendi dili, kendi sahibi olduğu görülüyor. Serinin 04. ve 05. yazısında konuştuğumuz Ordering, Inventory, Payments ve Shipping bounded context'leri bu bölgelerin üzerine oturuyor. Big Picture bounded context'i "çizmez"; adayları gösterir. Kesin sınır, context map konuşulurken ve kodda verilir.
Process Modeling: Policy'ler ve Read Model'ler
Ertesi gün daha küçük bir grupla yalnızca "sipariş verildi → kargoya verildi" aralığına odaklanılıyor. Bu seviyede her event'in solunda "bunu ne tetikledi?" sorusunun cevabı duruyor: bir actor'ün verdiği command, ya da önceki bir event'e tepki veren bir policy. Brandolini bu dizilimi bir dilbilgisi gibi okur: actor bir read model'e bakar, command verir; command bir sistemde ya da aggregate'te işlenir; sonuç bir event'tir; event bir policy'yi tetikler, policy yeni bir command üretir.
Bookstore'da iki önemli şey bu seviyede netleşiyor. Birincisi, akıştaki policy'lerin çoğu insan değil otomasyon: "OrderPlaced olunca stok ayır" ve "StockReserved olunca ödemeyi al" kimsenin onay vermesini beklemiyor. İkincisi, dünkü hot spot'un cevabı bir policy olarak geliyor: "OutOfStock olunca siparişi iptal et ve müşteriye haber ver". Ada ile Linus'un tartışması bir iş kuralına dönüşüyor: para, stok ayrıldıktan sonra çekilir. Bu karar ödeme sağlayıcısının gecikmesi için de bir cevap gerektiriyor; Mimariye Giriş serisinin "Quality Attribute Senaryoları: 'Hızlı Olsun'u Ölçülebilir Bir Gereksinime Çevirmek" yazısında yazdığımız "ödeme sağlayıcısı 5 saniyede cevap vermezse sipariş PaymentPending olur" senaryosu bu tartışmanın ürünü.
Read model'ler de burada ortaya çıkıyor. Linus'un toplama listesi (StockReserved olmuş ama OrderShipped olmamış siparişler) ve müşterinin "Siparişlerim" ekranı. İkisi de aynı event'lerden besleniyor, ama ikisi de farklı soruyu cevaplıyor. Bu ayrım ileride CQRS (Command Query Responsibility Segregation) ve Event Sourcing serisinin konusu.
Software Design: Duvardan Koda
Son seviyede artık tek bir bounded context'in ve onun ekibinin içindeyiz. Duvardaki notlar neredeyse birebir tiplere dönüşür: mavi sticky'ler command record'larına, turuncu sticky'ler event record'larına, mor sticky'ler event'i command'e çeviren kurallara. Aşağıdaki kod bir mimari önerisi değil, duvarın derlenebilir bir fotoğrafı:
namespace Bookstore.Ordering;
public readonly record struct OrderId(Guid Value)
{
public static OrderId New() => new(Guid.CreateVersion7());
}
// Mavi sticky'ler: command'ler (bir actor'ün ya da policy'nin niyeti)
public interface ICommand;
public sealed record PlaceOrder(OrderId OrderId, string Isbn, int Quantity) : ICommand;
public sealed record ReserveStock(OrderId OrderId, string Isbn, int Quantity) : ICommand;
public sealed record CapturePayment(OrderId OrderId) : ICommand;
public sealed record CreateShipment(OrderId OrderId) : ICommand;
public sealed record CancelOrder(OrderId OrderId, string Reason) : ICommand;
// Turuncu sticky'ler: domain event'ler (geçmiş zaman, olmuş bitmiş)
public interface IDomainEvent;
public sealed record OrderPlaced(OrderId OrderId, string Isbn, int Quantity) : IDomainEvent;
public sealed record StockReserved(OrderId OrderId) : IDomainEvent;
public sealed record OutOfStock(OrderId OrderId, string Isbn) : IDomainEvent;
public sealed record PaymentCaptured(OrderId OrderId) : IDomainEvent;
public sealed record OrderShipped(OrderId OrderId) : IDomainEvent;
public sealed record OrderCancelled(OrderId OrderId, string Reason) : IDomainEvent;
// Mor sticky'ler: policy'ler ("X olduğunda Y yap")
public static class Policies
{
public static ICommand? WhenThen(IDomainEvent @event) => @event switch
{
OrderPlaced e => new ReserveStock(e.OrderId, e.Isbn, e.Quantity),
StockReserved e => new CapturePayment(e.OrderId),
PaymentCaptured e => new CreateShipment(e.OrderId),
OutOfStock e => new CancelOrder(e.OrderId, "OutOfStock"),
_ => null
};
}
Policy'lerin bir switch expression olması tesadüf değil. Her policy "bu event geldiğinde şu command'i üret" der ve başka hiçbir şey bilmez; stok kaç, ödeme sağlayıcısı kim, onu ilgilendirmez. Duvarda iki sticky arasındaki ok neyse, kodda bu satır o.
Duvarı çalıştırmak için, sarı sticky'lerin yerine geçen basit bir karar mantığı ve bir döngü yeterli. Dune'dan (ISBN (International Standard Book Number) 9780441172719) depoda üç kopya var, iki müşteri ikişer kopya istiyor:
using Bookstore.Ordering;
Dictionary<string, int> stock = new() { ["9780441172719"] = 3 };
Run(new PlaceOrder(OrderId.New(), "9780441172719", 2));
Run(new PlaceOrder(OrderId.New(), "9780441172719", 2));
void Run(PlaceOrder placeOrder)
{
Console.WriteLine($"--- {placeOrder.Quantity} x {placeOrder.Isbn}");
ICommand? command = placeOrder;
while (command is not null)
{
IDomainEvent @event = Handle(command);
Console.WriteLine($"{command.GetType().Name,-15} -> {@event.GetType().Name}");
command = Policies.WhenThen(@event);
}
}
// Sarı sticky'lerin (aggregate/constraint) yerine geçen basit karar mantığı
IDomainEvent Handle(ICommand command) => command switch
{
PlaceOrder c => new OrderPlaced(c.OrderId, c.Isbn, c.Quantity),
ReserveStock c when stock[c.Isbn] >= c.Quantity => Reserve(c),
ReserveStock c => new OutOfStock(c.OrderId, c.Isbn),
CapturePayment c => new PaymentCaptured(c.OrderId),
CreateShipment c => new OrderShipped(c.OrderId),
CancelOrder c => new OrderCancelled(c.OrderId, c.Reason),
_ => throw new NotSupportedException(command.GetType().Name)
};
StockReserved Reserve(ReserveStock c)
{
stock[c.Isbn] -= c.Quantity;
return new StockReserved(c.OrderId);
}
--- 2 x 9780441172719
PlaceOrder -> OrderPlaced
ReserveStock -> StockReserved
CapturePayment -> PaymentCaptured
CreateShipment -> OrderShipped
--- 2 x 9780441172719
PlaceOrder -> OrderPlaced
ReserveStock -> OutOfStock
CancelOrder -> OrderCancelled
İkinci sipariş duvardaki hot spot'un tam olarak yaşandığı yer: ReserveStock reddedildi, OutOfStock event'i Process Modeling'de eklediğimiz policy'yi tetikledi ve sipariş para çekilmeden iptal edildi. Bu çıktı bir test senaryosu olarak da okunabilir. Domain expert'e "Dune'dan üç kopya varken iki müşteri ikişer isterse ne olmalı?" diye sorup cevabı bu satırlarla karşılaştırabilirsin.
Buradaki Handle method'u kasıtlı olarak kaba. Stok kontrolü bir Dictionary'de, ödeme her zaman başarılı, ISBN düz bir string. Gerçek karar mantığının nereye ve hangi kurallarla yerleşeceği serinin tactical design bölümünün konusu: 07'de Isbn ve Money value object'leri, 08'de invariant'lar, 09'da aggregate sınırları, 12'de event'lerin SaveChanges sırasında dağıtılması.
Duvardan Aggregate Adaylarına
Sarı (constraint) sticky'leri yerleştirirken sorulacak soru şu: hangi command'ler aynı kuralı korumak zorunda? PlaceOrder, CancelOrder, CancelItem ve RequestReturn aynı sorulara bakıyor: sipariş hangi durumda, hangi satırlar var, kargoya verildi mi? Bu dört command'in arkasında tek bir şey var, Order. ReserveStock ise tamamen başka bir soruya bakıyor: bu ISBN'den rafta kaç kopya var ve kaçı ayrıldı? Onun arkasında StockItem duruyor.
Bu ayrım duvarda görünür olduğu için sonraki tartışma da kolaylaşıyor. Order ile StockItem arasında bir policy ve bir event var, aynı sticky kümesi değil. Yani sipariş vermek ile stok ayırmak aynı transaction'da olmak zorunda değil; arada bir gecikme kabul edilebilir ve bu gecikmenin bedelini (OutOfStock'ta iptal) iş tarafı duvarda kabul etti. 09. yazıda bu kararı Vaughn Vernon'un aggregate kurallarıyla tekrar ele alacağız.
Bir uyarı: duvardaki her sarı sticky bir class, her renkli sütun bir microservice değil. Duvar sana adayları ve gerekçelerini verir; class'lar ve sınırlar kodda, test'lerle ve gerekirse yeni bir oturumla netleşir.
Uzaktan Oturum ve Alternatifler
Ekip aynı odada değilse oturum Miro ya da Excalidraw gibi bir online whiteboard'da yapılabilir. Fiziksel duvardan farklı olarak dijital tahtada birkaç şeyi önceden hazırlaman gerekir: renk sözlüğünü tahtanın sol üstüne sabitlenmiş bir legend olarak koy, her katılımcıya kendi sticky rengini değil herkese aynı renk setini ver, timeline'ı yatay bir ok olarak önceden çiz. Oturumu 90 dakikalık bloklara böl; ekran başında üç saatlik kaotik keşif kimseye iyi gelmiyor. Bir facilitator ve bir de tahtayı düzenli tutan ikinci kişi olsun. Kamera açık olsun; Big Picture'ın asıl verimi, Linus'un bir event'e bakıp "bu öyle olmuyor" dediği an.
EventStorming her soruya en iyi araç değil. Tek bir user story'nin kurallarını netleştirmek istiyorsan Matt Wynne'in 2015'te tanıttığı Example Mapping daha hızlı: sarı kartta story, mavi kartlarda kurallar, yeşil kartlarda her kuralın örnekleri, kırmızı kartlarda cevaplanamayan sorular. "Kitap başına en fazla 10 adet" kuralının sınır örneklerini konuşmak için yirmi dakikalık bir Example Mapping, sekiz metrelik bir rulodan daha verimli. Domain expert sticky'lerle değil hikâyeyle düşünüyorsa Stefan Hofer ve Henning Schwentner'ın Domain Storytelling yöntemi daha doğal gelir: bir senaryoyu "Grace, iade talebini alır, kargo etiketini müşteriye gönderir" gibi cümlelerle, actor ve work object ikonlarıyla çizersin. Üçü birbirine rakip değil; Big Picture için EventStorming, tek bir süreci derinlemesine anlatmak için Domain Storytelling, tek bir story'nin kabul kriterleri için Example Mapping iyi bir başlangıç.
Sık Düşülen Tuzaklar
Odada yalnızca developer'lar. Domain expert'siz bir EventStorming, developer'ların zaten bildiklerini renkli kâğıtlara yazdığı pahalı bir toplantıdır. Grace ve Linus gelmiyorsa oturumu erteleyin.
Çok erken Software Design'a atlamak. İlk saatte "bu Order aggregate'inde mi olmalı?" sorusu geliyorsa durdur. Command'ler ve aggregate'ler event'ler netleşmeden yerleştirilirse duvar, mevcut veritabanı şemasının bir kopyasına döner.
CRUD (Create, Read, Update, Delete) kokan event'ler. OrderUpdated, StatusChanged ya da OrderSaved bir domain expert'in ağzından çıkmaz. Bu isimleri gördüğünde "ne değişti, neden?" diye sor; cevap genellikle OrderCancelled ya da ShippingAddressChanged gibi gerçek bir event'tir. Benzer şekilde RowInserted gibi teknik event'ler duvara ait değil.
Hot spot'u oracıkta çözmeye çalışmak. Ada ile Linus'un tartışması yirmi dakika sürerse oturumun geri kalanı kaybolur. Kırmızı notu yapıştır, devam et, oturum sonunda en önemli üç hot spot için ayrı bir konuşma planla.
Duvarı doküman sanmak. Fotoğrafı çek, ama timeline'ın kendisi canlı bir doküman değil. Kalıcı olması gereken şey kodda (event ve command isimleri), glossary'de ve context map'te yaşamalı. Altı ay sonra fotoğrafa bakan biri neyin hâlâ geçerli olduğunu bilemez.
Küçük Bir Deneme
★☆☆ Duvara "iade" bölgesi için eksik event'leri ekle: müşteri kitabı geri gönderdi, depo kitabı teslim aldı, kitap hasarlı çıktı. Her birinin ismini glossary'deki dille (Return mı Refund mı?) yaz ve hangi bounded context'e düştüğünü belirt.
★★☆ Koda PaymentFailed event'ini ve "ödeme başarısız olunca stok rezervasyonunu bırak ve siparişi iptal et" policy'sini ekle. WhenThen bir event'e karşılık iki command üretmek zorunda kalırsa imzası nasıl değişir?
★★★ Kendi ekibinle 45 dakikalık bir mini Big Picture yap: konu, sizin uygulamanızda bir kullanıcının ilk satın alımı. En az beş hot spot ve üç pivotal event çıkar; pivotal event'lerin arasındaki bölgelerin bugünkü kod tabanınızdaki project ya da namespace'lerle örtüşüp örtüşmediğine bak.
Duvardan çıkan Order, StockItem, Money ve Isbn artık kod olmayı bekliyor. Bir sonraki yazıda tactical design'a giriyoruz: entity'yi identity'si, value object'i değer eşitliği tanımlıyor; C# record'ları, strongly typed id'ler ve Entity Framework Core (EF Core) 10'un complex type'ları ile bu iki kavramı koda döküyoruz.