Business Logic Organization Patterns

Semih Şahan
10 min readFeb 25, 2023

--

Merhabalar, bugün sizlere Hexagonal Arhitecture ile başlayarak DDD(Domain Driven Design), event storming ve event-centric messaging kavramları ile eğer complex bir business logic sahipseniz nasıl bir tasarım yapmalıyız konusunda, sadece avantajlardan bahsederek değil, dezavantajlarından da bahseden bir anlatım yapacağız, iyi okumalar dilerim..

  • Hexagonal architecture’da inbound ve outbound adapter’larımız vardır, “port and adapter” pattern olarak da geçer bu mantık.
  • Inbound Adapter: Service’imize dışardan gelen request’leri karşılayan kısımdır.
  • Outbound Adapter: Service’mizin dışarısı ile yaptığı etkileşimin bulunduğu kısımdır
  • REST Api Adapter: Inbound adaptera örnek verilebilir. Dışarıdan gelen rest call’ları karşılayan kısımdır.
  • OrderCommandHandlers: Inbound adaptera örnek verilebilir. Order domain saga’ları ile ilgili gelen eventleri karşılayan consume eden kısımdır. (Saga ile ilgili yazımıza bakabilirsiniz: https://medium.com/@semihshn/saga-pattern-fce2f104d3e)
  • Database Adapter: Outbound adaptera örnek verilebilir, veritabanına veri yazdığımız veya okuduğumuz infrastructure kodlarını burada bulundurabiliriz.
  • Domain Event Publishing Adapter: Outbound adaptera örnek verilebilir, message brokerlar ile event publish ettiğimiz kısımdır.

Ekstra olarak Hexagonal Architecture kod örneği: https://github.com/semihshn/driver-service

  • Business Logic tasarlamak için 2 farklı yöntem vardır Transaction script ve Domain model
  • Transaction script: Her bir request procedural transaction’ların bir listesi ile işlenir
  • Domain model: Business logic state ve behavior sahip class’lardan oluşur
  • Object-orianted bir tasarımda, business logic bir object modelinden, nispeten küçük class’lardan oluşan bir ağdan oluşur(bunun ne demek olduğunu yazının devamında ayrıntılı açıklıyoruz..)
  • Günümüzde çoğu firmanın kullandığı yöntem procedural programing’dir, buda Transaction Script Pattern olarak geçer aslında, OOP’nin tüm nimetlerinden yararlanmak yerine çok az bi özelliğinden yararlanır, service class’lar vardır, controller’dan gelen her request için bir method vardır ve bu methodlarda business logic tutulur, service class’lar state tutmaz state’leri entity class’lar tutar, service class’lar transaction scriptleri, yani davranışlar(behavior) tutar,Prosedürel yaklaşımın basitliği oldukça baştan çıkarıcı olabilir. Sınıfları nasıl düzenleyeceğinizi dikkatlice düşünmek zorunda kalmadan ezbere kod yazabilirsiniz, çoğu kişi buna kanar ama complexity oranı yüksek bussines logic’leri yönetmesi çok zor olur
  • Sonuç olarak, son derece basit bir uygulama yazmıyorsanız, prosedürel kod yazmanın cazibesine direnmeli ve bunun yerine Domain Model Pattern’i uygulamalı ve nesne yönelimli bir tasarım geliştirmelisiniz.
  • Transaction Script Pattern’de olduğu gibi Domain Model Pattern’de de bir OrderService sınıfının her request/sistem işlemi için bir methodu vardır. Ancak bu methodlar genelde çok basittir, Transaction Script Pattern’deki gibi complexity değeri çok yüksek olmaz bunun nedeni business logic’in büyük kısmı domain object’lerde olur. Domain object’lerin state’leri private olur ve state’lere direkt erişilemez, dolaylı yoldan getter methodlarıyla vs erişilebilir(Thread Safe Objects). Bu şekilde tasarım OOP’nin daha fazla nimetinden yararlanır ve kodun anlaşılması ve yönetilmesi daha kolay olur. Büyük complex methodlardan oluşan class’lar yazmak yerine daha küçük ve sayıca daha fazla class oluşturarak durumu yönetmek, divide and conquer prensibine uygundur. Ayrıca Account, BankingTransaction, and OverdraftPolicy gibi sınıflar gerçek dünyayı yakından yansıtarak tasarımdaki rollerinin anlaşılmasını kolaylaştırır. Kodu dökümante etme zorluğu azalır, karmaşık dökümanlar yazmaya gerek kalmaz çünkü paketler ne olduğunu anlatır.
  • Diğer bir avantajı nesne yönelimli tasarımımızın test edilmesi daha kolaydır: her sınıf bağımsız olarak test edilebilir ve test edilmelidir.
  • En önemli faydası ise tasarımımız OOP daha uygun olduğu için Gang Of Four’un bilinen faydalı Patternlerini uygulama alanlarımız çoğalacak ve sistemimiz daha SOLID bir yapıda olacaktır

DDD Hakkında

  • Eric Evans’ın Domain-Driven Design (Addison-Wesley Professional, 2003) adlı kitabında açıklanan DDD, OOD’nin geliştirilmiş halidir ve karmaşık iş mantığı geliştirmeye yönelik bir yaklaşımdır.

Entity

  • Persistent identity değeri olan bir object. Bu yüzden fieldları aynı değerlere sahip iki entity hala farklı objelerdir. Bir Java EE uygulamasında, JPA’daki @Entity anotasyonu kullanılarak sürdürülen sınıflar genellikle DDD entity’leridir.

Value Object

  • Farklı entity’lerde bulunabilen, farklı field’lardan oluşan objeler, bunların entity’ler gibi persistent identity değerleri yoktur, Money class’ı buna örnek verilebilir, amount ve currency fieldlarından oluşur ve id fieldı yoktur, gerekte yoktur

Factory

  • Bir nesnenin oluşturulma logic’ini implement eden objelerdir, factory class’lar sayesinde concrete class’lar gizlenerek sistemdeki abstraction seviyesi artırılabilir, factory class’ları state tutmadığı için genelde static method olarak implementasyonu sağlanır

Repository

  • Veritabanına erişmemizi sağlayan class’lar

Service

  • Business logic implementasyonunu sağladığım class’lar

Ve DDD içinde çok bilinmeyen bir kavram daha vardır, Aggregate…

Designing a domain model using the DDD aggregate pattern

  • Genelde sınıfları package içinde organize ederiz ama bir class içinde organize etsek buna da aggregate desek nasıl olur ?
  • Aggregate kullanmadığımızda başımıza gelebilecek kötü bir senaryo;
  • Mary ve Sam isminde iki kullanıcı olsun, bizim sistemimizde de minimum sipariş tutarı kontrolü olsun, sistemde kullanıcı alışverişini tamamlayıp satın al butonuna bastıktan sonra minimum sipariş tutar kontrolü yapılıyor, Mary ve Sam için aynı anda ORDER ve ORDER_LINE_ITEM tablolarına select atıyoruz, Mary satın al butonuna Sam’den önce basıyor ve Mary için minimum tutar kontrolü yapıyoruz ve Mary bu şartı sağladığı için Mary’nin satın aldığı ürünler için quantity bilgisini -1 yaparak update işlemi yapıyoruz, daha sonra Sam’de satın al butonuna bastığında Sam için select yaptığımız dataları Mary’nin işlemi değiştirmişti, güncel olmayan datalara göre Sam için limit kontrolü yaparız…
  • Aggregate’ler root entity, root entity ile ilgili diğer entity’ler ve value object’lerden oluşur. Mesela Order, Consumer ve Restaurant gibi bunların çoğu aggregate’dir.
  • Yukarıdaki görselde de görüldüğü üzere ilişkili entity’ler yani boundary içindeki entity’ler tek bir aggregate object’de toplanır ve eager stratejide kullanılır böylece bir nesne veritabanından silineceği, eklenceği veya başka işlemler yapılacaği zaman ilişkili diğer nesnelerede ilgili işlemler uygulanır. Her bir entity güncellemek yerine aggregate’i güncellemek consistency sorunlarını çözer

Aggregate’lerin sahip olması gereken özellikler;

  • Aggregate içerisindeki entitiylere sadece aggregate objectleri üzerinden ulaşılabilir.
  • Her Aggregate aslında bir microservice yani aggregate’ler aynı jvm üzerinde çalışmadığı için object referansıyla bir aggregate diğerine ulaşamaz bu yüzden Aggregate’ler birbirlerini nesne referansından değil, her aggregate’in sahip olduğu primary key fieldı ile bulur
  • Figure 5.7 de Aggregate Y ve Aggregate Z ile ilgili işlemleri aynı transaction içerisinde yapabiliriz, bu mümkün ama yapmamalıyız, özellikle NoSql(çünkü çok fazla transaction supportu yok) database’lerde bu soruna yol açabilir, saga pattern’de de her step tek bir aggregate işleminin gerçekleştiği bir transaction olmalı yani her transaction sadece 1 tane aggregate create veya update işlemi yapabilir

Aggregate granularity

  • Aggregate büyüklüğünü artırabiliriz mesela figure 5.5'deki gibi değilde figure 5.8 deki gibi yaparsak consumer ve order aynı aggregate içinde olduğu için transaction’lar daha hızlı gerçekleşir ama dezavantaj olarak scalability yeteneğimizi azaltmış oluruz.

Designing business logic with aggregates

  • Aggregate-base tasarım figure 5.9'daki gibi olmaktadır, business logic’in çoğunluğunu aggregate’ler oluşturmaktadır, service’ler inbound adapter’lar(REST veya CommandHandler) tarafından invoke edilir, service’lerin kullandığı repository’ler aggregate’leri(dikkat Entity demiyorum aggregate diyorum!) save veya retrieve etmektedir, her repository port and adapter patterne uygun olarak outbound adapter’lar ile implementasyonu sağlanır

Publishing domain events

  • DDD felsefesiyle baktığımızda olaya domain event diyince aggregate’in başına bir şeyler gelecek demektir :) event diyince bir state değişikliği oldu demektir

Why publish change events?

  • Bir state değişikliği olduğunda domain event publish etmeliyiz çünkü diğer microservice’ler veya aynı microservice içinde olan farklı component’ler state değişikliğinden haberdar olup kendilerini de o state değişikliğine göre düzenlemeleri gerekir, mesela state değişikliğine göre business logic’deki sonraki stepe geçilir veya data consistincy sağlamak için gerekli değişiklikler sistemdeki diğer component’lerde de yapılır, mesela CQRS pattern’de bu yapılır

What is a domain event?

  • Domain event dediğimiz şey aslında isimlendirmesinde geçmiş zaman fiili kullandığımız class’lardır, isimlendirmelere örnek vermek gerekirse OrderCreated, bu class anlamlı property’lere sahip olmalıdır mesela, orderId olmalıdır, timestamp bilgisi, belki state değişikliğini kimin yaptığına dair user bilgisini barındırabilir, auditing işlemleri için gerekli olabilir.
  • Domain event’lerimiz için yukarıdaki görseldeki gibi generic bir alt yapı sağlanabilir.
  • DomainEvent, tüm eventler için marker interface.
  • OrderDomainEvent ise Order domaini ile ilgili olan event’ler için marker interface.
  • DomainEventEnvelope ise metaData ve event barındıran wrapper class

Event enrichment

  • OrderCreated eventi consume eden kısmı düşünelim, bu kısım order detaylarına sahip olmalı ve işlemler yapmalı, burada iki yöntem var consumer Order service rest call yapar ilgili order detaylarını alır ama bunun network latency’den dolayı cost’u yüksektir, bunun yerine order event class’larımız order detaylarınıda barındırsa daha güzel olur dimi, iki iş yapmamış oluruz :)
  • İşte burada gördüğümüz gibi event class’larımız consumerın ihtiyacı olan detay datalarıda barındırmalıdır
  • Bu yönteminde şöyle bir dezavantajı var; consumer’ların order detayı konusunda ihtiyaçları değişebilir ve bizim event class’larındaki property’leri değiştirmemiz projemizdeki başka nereleri etkileyeceğini bilemeyebiliriz..(DEZAVANTAJ)

Identifying domain events

  • Domain eventleri tanımlama konusunda birkaç farklı yöntem vardır, uygulamamızdaki senaryolara bakarak gerekli domain eventler tanımlanabilir veya event storming yapılabilir, event storming complex domaini anlamak için event-centric bir iş yapış biçimi diyebiliriz. Domain expert’ler bir odaya toplanır, yapışkanlı renkli not kağıtları ve geniş bir tahta kullanılarak, aggregate ve event’leri belirleyerek ilerlenir ve sonuçta sistem tasarlanmış olur
  • Event Storm Aşamaları;
  • Beyin fırtınası yapıldığı bölüm; domain expert’lerden domain event’ler hakkında beyin fırtınası yapmaları ve turuncu yapışkanlı kağıtları tahtaya yapıştırarak sistemin taslağını oluşturmaları istenir
  • Domain event’leri tetikleyen şeylerin neler olduğunun belirlendiği bölüm; domain expert’lerden tahtadaki turuncu kağıtlarda yazılı olan eventleri aşşağıdakilerden hangisinin tetiklediği sorulur ve onunla ilgili renkli kağıt ilgili turuncu kağıtların kenarlarına yapıştırılır

Kullanıcı aksiyonları(Mavi)

External system(Mor)

Başka bir domain event(Turuncu)

Belli bir zamanın geçmesi

  • Aggregate’lerin tanımlanması bölümü; her bir eventi publish eden ve consume eden aggregate’lerin belirlenmesi istenir, aggregate’ler Sarı yapışkanlı kağıt ile ifade edilir

Generating and publishing domain events

Generating Domain Events

  • Domain eventler aggregate’ler tarafından publish edilebilir. Aggregate’ler state’lerinin değiştiğini ve değiştiğinde hangi event publish edeceğini bilir ama aggregate’ler direkt messaging api kullanamaz çünkü kullanması için messaging api depenceny injection yapması gerekir ve bu durum business logic ile infrastructure kodunun iç içe geçmesine yol açar buda istemediğimiz bir durum, bu yüzden aggregate service class’ı çağırır, service class ilgili messaging api dependency kendi içinde inject eder ve çağırır, yani aggregate kendinde state değişikliği olduğunda event generate eder ve service class’a yollar, service class messaging api aracılığıyla event’i publish eder
  • Yukarıdaki kodda görüldüğü gibi aggregate’de event list şeklinde döndüren accept methodumuz var

How To Reliably Publish Domain Events

  • Domain event’leri publish etmeden önce outbox table kaydedersek, asenkron iletişimde başımıza gelebilecek kötü durumlardan kurtulabiliriz, outbox patterne bakılabilir bunun için
  • Kitap domain event publish metodları her yerde kullanılacağı için kod tekrarı ve clean code’a uygun olması için bir tasarım modeli öneriyor; AbstractAggregateDomainEventPublisher isminde generic bir aggregate class yazın diyor, tüm publisher class’lar ilgili abstract class’ı extend etsin ve constructorunda super keyword ile ilgili publish method kullanılsın deniyor. İlgili tasarım kod örnekleri şu şekilde;

AbstractAggreageDomainEventPublisher

Publisher

  • Message broker client’lerini kullanarak consumer’lar oluşturuyoruz ve publish edilen event’leri handle ediyor, handle eden class’lara eğer saga’lar tarafından çağırılacak bir şeyse …commandHandler isimlendirmesi veriliyor

Kitchen Service Business Logic Diagram

  • Inbound Adapter’lar

REST API

KitchenServiceCommandHandler: saga’ların tetiklediği consumer class’lar

KitchenServiceEventConsumer: Restaurant Service’in publish ettiği event’leri yakalayan consumer

  • Outbound Adapter’lar

DB adapter

DomainEventPublishingAdapter: DomainEventPublisher(Port) interface’ini implement eden Ticket domain event publish eden class

Structure Of The Ticket Class

  • Restaurant objesi tutmak yerine Long türünde Restaurant Id tutuluyor !!
  • readyBy fieldı order’ın tahminen ne zaman hazır olacağını belirtiyor

acceptTime, preparing-Time ve pickupTime fieldları Order’ın oluşma history’sini tutmak için kullanılır

Behavior Of The Ticket Aggregate

  • Aggregate Ticket class içinde static create() methodu vardır, bu factory bir methoddur
  • Restaurant order state’ini update ettiğinde çağırdığı methodlar şunlardır;

accept() -> restaurant order’ı accept ettiğinde kullanılır

preparing() -> Restaurant Order’ı hazırlanmaya başladığında çalışır, bu çalıştıktan sonra order’da iptal veya değişiklik yapılamaz

readyForPickup() -> order hazır ve kurye tarafından alınabilir olduğunda çalışır

The Kitchen Service Domain Service

  • Kitchen Service içindeki accept() methodu, Restaurant Service’deki accept() methodu çalıştığında tetiklenir

The Kitchen Service Command Handler Class

Order Service Business Logic

  • Inbound Adapter’lar

REST API: OrderService çağırır

OrderEventConsumer: Restaurant Service’den gelen event’leri consume eder.

OrderCommandHandlers: saga’lar tarafından publish edilen event’ler consume edilir burada, order update işlemleri yapılan yerdir

SagaReplyAdapter: Saga reply topiclerine subscribe olur, reply durumunda yapılması gereken business logic işletir

  • Outbound Adapter’lar

DB adapter

DomainEventPublishingAdapter: DomainEventPublisher interface implement olur ve domain eventler publish edilir

OutboundCommandMessageAdapter: CommandPublisher interface implement eder ve saga parçalarına commmand publish eder

Structure Of Order Aggregate

Order Aggregate State Machine

  • Her Order Service methodu ilgili operasyonun gerçekleştirilebilmesi için önce Order state bilgisini PENDING’e çeker ve işlemini gerçekleştirilebilir olup olmadığına dair bir saga kullanır ve işlem gerçekleştirilebilirse akış devam eder
  • Mesela eğer operasyonun gerçekleştirilmesine izin verilmedi(neden verilmediği gibi senaryoları Saga Pattern yazısından)anlatmıştık, state CANCEL_PENDING çekilir ve o ana kadar değiştirilen farklı aggregate state’leri varsa eski haline getirilir ve CANCELED state yapılır

Order Aggregate’s Methods

  • noteApproved() methodu ilgili order’ın state bilgisini APPROVED çekiyor ve authorized olduğuna dair diğer ilgili component’lere event publish ediyor
  • noteRejected() methodu ilgili order’ın state bilgisini REJECTED çekiyor ve event publish ediyor
  • revise() methodu ilgili siparişin toplam ücreti minimum sepet tutarından az mı kontrolü yapıyor eğer az ise exception throw ediyor, değilse order state’i REVISION_PENDING duruma çekiyor ve event publish ediyor, ilgili event’ler Kitchen Service ve Accounting Service update ettikten sonra confirmRevision() methodu çalışır ve revision tamamlanır

Order Service Class

  • Burada dikkat edilmesi gereken şudur; order direkt update edilmiyor, saga’lar kullanılarak update ediliyor ve data consistency sağlanıyor

Summary

  • Basit bir business logic implementasyonu yapıyorsak, Procedural Transaction Script Pattern idealdir ama complex bir business logic varsa object-oriented Domain Model Pattern değerlendirilmelidir
  • Aggregate kullanımı domaini modulerize etme ve bağımlılık’lardan kaynaklı sorunların ortadan kaldırılmasında güzel bir tasarım modeli sunar bizlere
  • Aggregate’lerin state değişiminde event paylaşabilir olduğunu unutmamalıyız.

Kaynak: https://microservices.io/book

--

--