Yazılım

Event’im Nasıl Olmalı? Event-Based Sistemler Hakkında Bazı Düşünceler

Bildiğimiz gibi yazılım geliştirme, özellikle günümüz ihtiyaçları karşısında kolay kolay bitmiyor. Yazılımı geliştirmenin yanı sıra, onun sürekli ayakta ve tutarlı bir şekilde çalışabilmesini sağlayabilmekte en büyük sorun ve sorumluluklarımızdan bir tanesi. Tahmin edebileceğimiz gibi bu gibi ihtiyaçlarda zamanla bizleri microservice mimarilerine, event-based sistemlere, distributed ortamlara doğru yöneltmektedir.

Makaleye başlamadan önce şunu unutmamalıyız ki her ne olursa olsun bu mimariler ve pattern’ler, tek bir atışta tüm problemlerimizi çözümleyecek bir gümüş kurşun değildir. Kendi doğrularımızı içerisinde olduğumuz business context’e göre belirlememiz gerekmektedir.

Seçimlerimizi eğer olabildiğince doğru bir şekilde gerçekleştirebilirsek gayet güzel işler başarabiliriz. Aksi bir durumda ise geçmiş tecrübelerime dayanarak söyleyebilirim ki seçimlerimizin getireceği bazı sorunları çözmeye çalışıyorken kendimizi bulabiliriz.

Bu makale kapsamında ise event-based sistemler üzerinde çalışırken, event’lerimizi nasıl şekillendirebileceğimiz hakkındaki bazı düşüncelerimi sizlerle paylaşmak istiyorum.

Amacımız

Öncelikle event kullanımındaki bazı amaçlarımızı hatırlayalım. Ben kısaca “modularity” diyerek ele almak istiyorum.

Gerek modüllerin farklı ekipler tarafından geliştirilebilmesi, gerekse de bağımsız deployment ve scale edilebilmesi gibi modularity, her anlamıyla içerisinde olduğumuz sistemin complexity’sini ve coupling’ini bölebilmemize olanak sağlamaktadır. Event’ler ise bu amaçlarımıza ulaşmaya çalışırken yararlanabileceğimiz en temel yapı taşlarımızdan birisidir.

Microservice mimarisi açısından uygulamalarımızı mümkün olduğunda küçük, decoupled parçalar halinde design ederiz. Event’leri ise bu uygulamalar arasındaki iletişimi, non-blocking ve asynchronous olarak sağlayabilmek için kullanırız. Böylece bir uygulama kendi domain context’i içerisindeki işleri tamamladıktan sonra, bir başka domain context içerisindeki işlerin de bağımsız olarak tamamlanabilmesine olanak tanımaktadır. Bağımsız olmak, birazdan değineceğimiz buradaki kilit noktamız.

Ne Olabilir?

Unutmamalıyız ki problemlerimize çözüm getiren her bir yöntemin, beraberinde getirdiği bazı farklı problemleri de olabiliyor. Aynı durum event’leri design ederken alacağımız kararlarda da mevcut.

Küçük ve decoupled olarak design ettiğimiz uygulamalarımıza daha çok özellikler eklemeye başladığımızda veya domain sınırlarımızı/implementasyon yöntemimizi doğru seçemediğimizde, uygulamalarımız distributed olarak coupled/bağımlı bir hale gelmeye başlıyor. Kısacası distributed monolith‘e doğru giden bir yolculukta kendimizi bulabiliriz.

https://sebiwi.github.io/assets/images/comics/2018-09-10-distributed-monolith.jpg

İsimlendirme

Günlük hayatımızda bir konunun daha anlaşılabilir olabilmesi için nasıl olabildiğince spesifik olmaya çalışıyorsak, event’leri isimlendirirken de olabildiğince spesifik olmaya çalışmalıyız. Generic event isimlendirmelerinden kaçınmak, diğer takımlar tarafından kullanılacak olan event’lerin daha anlaşılabilir olmasını sağlayacaktır.

Örneğin aşağıdaki gibi bir event’e sahip olduğumuzu düşünelim.

public class ProductUpdatedEvent
{
    public Guid ProductId { get; set; }
    public Guid ModifiedByUserId { get; set; }
    public DateTime ModifiedOn { get; set; }
}

Anlıyoruz ki ürün üzerinde bir güncelleme işlemi gerçekleştirildi. Peki bu işlem nasıl bir işlemdi? Yeni bir resim mi eklendi yoksa ürün açıklaması mı değiştirildi?

Belki burada gerçekleştirilen spesifik bir aksiyon, bir başka domain içerisinde bulunan farklı bir aksiyonun da gerçekleştirilebilmesini sağlayacaktır.

Kısacası karışıklıklara yer vermemek için olabildiğince event’leri isimlendirirken spesifik olmamız veya takımlar arası bazı kavramlar üzerinde anlaşmış olmamız gerekmektedir.

Event’im Nasıl Olmalı?

Bu sorunun maalesef tek bir cevabı, yöntemi bulunmamaktadır. Tamamen içerisinde çalıştığınız domain’e, boundery’lerine ve ihtiyaçlarınıza göre değişiklik göstermektedir. Event içeriğini, boundery’lerini design etmek çoğu zaman kafa karıştırıcı olabildiği gibi overengineer edilebilmeye de oldukça açık konular.

Event içeriğini design ederken “Fat Events“, “Thin Events“, “Delta Events“, “Notification Events“, “Event-carried State Transfer” gibi birbirlerine benzeyen kavramlarla karşılaşabilirsiniz. Ben çok fazla kafa karışıklılığına yer vermeden, event içeriğini design ederken faydalanabileceğimiz iki basit yöntemden bahsetmek istiyorum.

Thin Events

Bu yaklaşım adından da anlaşılabileceği üzere içerisinde id’ler dışında çok fazla bilgi barındırmamaktadır. Mevcut domain içerisinde gerçekleşmiş olan bir aksiyondan, diğer dış sistemlerin de haberdar olabilmesini sağlamaktadır.

Örnek olarak daha önceki makalemde ele aldığım e-ticaret senaryosunu ele alalım.

public class OrderCreatedEvent
{
    public Guid OrderId { get; set; }
    public Guid UserId { get; set; }
    public Guid WalletId { get; set; }
    public DateTime CreatedOn { get; set; }
}

Order domain’i içerisinde bir sipariş oluştuğunda, “OrderCreated” adında bir event publish ettiğimizi düşünelim.

Order workflow’unun tamamlanabilmesi için bu değişim ile ilgilenen “Stock“, “Payment” gibi diğer service’ler, bu event’i dinlemeli ve gerekli işlemlerini tamamlamaları gerekmektedir. Diğer service’ler işlemlerini tamamlayabilmeleri için yeterli data’ya sahip değillerse, bu örnekte değiller, event içerisinde bulunan id’leri kullanarak ilgili domain’lerin resource API‘larından gerekli data’ları elde etmeleri gerekmektedir.

Örneğin “Payment” service’i için ödemenin geçekleşeceği cüzdanın bilgileri veya “Stock” service’i için allocate edilecek olan ürünlerin id’lerini düşünebiliriz. Farklı bir örnek vermek gerekirse eğer CQRS yaklaşımını uyguluyor olabilir ve read-modellerini bu yöntem ile sync tutuyor olabilir veya farklı domain’ler kendi projection data’larını oluşturuyor da olabilir.

Uygulaması ve başlaması en basit yöntemlerden birisidir. Özellikle publish ettiğimiz event’i dinleyen subscriber’ların ilgili değişimden etkilenen data’nın en son güncel hali ile çalışmaları kritik ise, thin events bu noktada oldukça yararlı olacaktır.

Bu yaklaşımın dezavantajı ise, event-based sistemler ile bölmeye ve azaltmaya çalıştığımız coupling’i, sistemler arasında farkettirmeden duruma göre geri getirmektedir. Çünkü ilgili değişim ile ilgilenen subscriber, çalışabilmesi için yeterli data’ya sahip değilse ilgili data’yı elde edebilmek için ilgili domain’in resource API‘larına erişmesi gerekmektedir.

Eğer ilgili resource’un API‘ı o anda çalışmıyor ise, değişim ile ilgilenen subscriber da işlemini yerine getiremeyecektir. Ayrıca değişim ile ilgilenen ne kadar çok subscriber varsa, bu subscriber’lar da ilgili resource’un API‘ına internal bir ek yük getirecektir.

Sistem içerisinde oluşabilecek olan bu internal yük’ü ve coupling’i minimum’a indirebilmek için ise her service, kendi projection data’larına sahip olabilirler. Böylelikle sistem içerisindeki değişim ile ilgilenen subscriber’lar, herhangi bir ihtiyaç karşısında ilgili resource’un API‘ını sorgulamaları gerekmeyecek ve publisher tarafında oluşabilecek failure durumlarından da etkilenmeyeceklerdir. Bulkhead isolation’a sahip olmak olarak da düşünebiliriz.

Ne yazık ki kulağa her ne kadar güzel bir çözüm gibi gelsede, bu çözümün de sisteme getireceği technical complexity ve eventual/data consistency de kabul edilmelidir. Çünkü ilgili service’lerin projection data’larını oluşturabilmek ve consistent tutabilmek için, ilgili domain’lerde meydana gelen ilgili event’leri dikkatlice handle etmeleri gerekmektedir.

Fat Events

Bu yaklaşımda ise thin events’in aksine ilgili event sadece id’ler barındırmak yerine, diğer subscriber’ların da çalışabilmesi için gerekli olan data’ları da barındırır.

Örneğin “OrderCreated” event’inin aşağıdaki gibi olduğunu varsayalım.

public class OrderCreatedEvent
{
    public Guid OrderId { get; set; }
    public List OderItems { get; set; }
    public UserDTO User { get; set; }
    public WalletDTO Wallet { get; set; }
    public DateTime CreatedOn { get; set; }
}

public class OrderItemDTO
{
    public Guid OrderItemId { get; set; }
    public Guid ProductId { get; set; }
    public string ProductName { get; set; }
    public string SKU { get; set; }
    public string Quantity { get; set; }
    public decimal Price { get; set; }
}

public class UserDTO
{
    public Guid UserId { get; set; }
    public string Name { get; set; }
    public string Surname { get; set; }
    public string EmailAddress { get; set; }
    public ShippingAddressDTO PreferredShippingAddress { get; set; }
}

public class ShippingAddressDTO
{
    public string Address { get; set; }
}

public class WalletDTO
{
    public Guid MasterpassWalletId { get; set; }
    public Guid MasterpassCardId { get; set; }
}

Bu senaryoda “OrderCreated” event’i içerisinde diğer subscriber’ların da çalışabilmesi için gerekli olan tüm data’lar bulunmaktadır. Böylelikle diğer subscriber’ların gerekli data’ları elde edebilmeleri için ilgili domain’lerin resource API‘larına erişmeleri veya projection data’larına sahip olmaları gerekmemektedir. Kısacası thin events’in aksine fat events, sistemler arasında bir coupling yaratmak yerine daha iyi bir decoupling, availability sağlamakta ve network latency‘sini azaltmaktadır.

Gördüğümüz gibi fat events kullanım senaryolarına göre oldukça faydalı bir yaklaşımdır. Fakat tahmin edebileceğimiz gibi bu yaklaşım da kusursuz bir şekilde gelmemektedir. Örneğin bu event ile ilgilenen subscriber’ların ilgili data’nın en son güncel hali ile çalışmaları kritik ise, bu yöntem çok da hoş olmayabilir. Çünkü event içerisindeki herhangi bir data, herhangi bir zamanda outdated olmuş olabilir.

Ayrıca fat events’in publisher’a getirdiği contract dependency’sini de görmezden gelemeyiz. Bu dependency ile event içerisinden herhangi bir data’yı istenilen bir zamanda, örneğin “Wallet” objesini, emin olmadan kolay bir şekilde silemeyiz.

Ne kadar zor ve complex kararlar değil mi? Bir sorunu çözebilmek için uyguladığımız yöntemin de sisteme getirdiği bir başka sorunu görüp, ona da çözüm sağlamamız gerekmektedir.

Özetleyelim

Event-based sistemlerin bizlere sağladığı modularity yetkinliği ile içerisinde bulunduğumuz business’ın complexity’sini ve coupling’ini minimize edip, bölebilmemize olanak sağlamaktadır.

Bu makale kapsamında ise event-based yaklaşımını içerisinde bulunduğumuz domain’e uygularken, event’lerimizi en basit halleriyle nasıl design edebileceğimizi tradeoff’ları ile göstermeye çalıştım.

Thin events mi yoksa fat events mi sorusuna yanıt verebilmek açıkçası oldukça zor. Tamamen içerisinde bulunduğumuz domain context’ine, boundery’sine ve ihtiyaç’a göre değişiklilik göstermektedir. Thin events gördüğümüz gibi sisteme bir dezavantaj olarak runtime coupling’i getirirken, avantaj olarak ise subscriber’ların ihtiyaç duydukları data’nın en güncel hali ile çalışmaları kritik ise bu sorunu adreslemektedir. Ayrıca başlaması ve uygulaması oldukça kolay olup, publisher’a herhangi bir contract dependency’si de eklememektedir.

Fat events ise daha iyi bir decoupling ve availability sağlamaktadır. Fakat fat events’in de publisher’a getirdiği dependency ve complexity gibi problemleri de kabul ediyor olmalıyız.

Peki ya siz hangi yöntemi tercih ediyorsunuz?

Referanslar

https://docs.microsoft.com/en-us/azure/architecture/guide/architecture-styles/event-driven?WT.mc_id=DT-MVP-5003382

İlgili Makaleler

Bir Yorum

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Başa dön tuşu