Product Domain Modernizasyonu

Sefik Sahin
LCW Digital
Published in
6 min readDec 12, 2022

Merhaba, bu makalemde e-ticaretin merkezinde olan product domain modernizasyonundan ve mimarisinden, veri yapısı, diğer domainler ile etkileşimde event driven hale dönüşümünden ve modernizasyon sürecinden bahsedeceğim.

E-ticaret sistemlerimizde birçok modernizasyon gerçekleştirdik, bunu öncelikle kendini modernize eden product domain’inin esnek yapısına borçluyuz. En temelde IIS ’de host edilen single instance çalışan ve scale edilemeyen hizmet yerine dockerize olan ve tüm modernizasyon sürecimizde kullandığımız Pars.Core adında .net core tabanlı bir framework kullanarak scalable bir microservice dönüşümü gerçekleştirdik.

İlk işimiz veri yapısından başlamak oldu, legacy yapıda RDBMS MSSQL de yaklaşık 20’yi aşkın tablolarda duran ve yurt içi, yurt dışı birden fazla dile ait verileri öncelikle tek bir document base data structure içerisinde tutma kararı verdik. Bunun en büyük nedeni ise veriye key value hızlı erişme ve atomicity oluşturmak oldu. Her bir ürün için dil bazlı kayıtların update etmek atomic olarak bizi zorlayacak ve bu kayıtları sync etmekte güçlük yaşayacaktık. Bunun üzerine veriyi document bazından (1M * language count) olarak büyütmek yerine sadece ürün adeti bazında (1M) bırakmamızda atomic yapıyı oluşturması önemli rol oynadı. Böyle bir veri tipine en uygun document base database olarak belki de şu anda birçok domain ’imizde kullandığımız, memory first çalışması, async bir database olması ve domaine uygun birçok özelliği bulunması sebebi ile Couchbase’i tercih ettik. Öncelikli hedefimiz veriye key value erişmek ve ihtiyaç olabildiğinde index’ler ile index ve query node’ları üzerinden hizmet sağlamak oldu. E-Ticaret merkezinde duran bir domain için performans en öncelikli maddemizdi.

Data dominates. If you’ve chosen the right data structures and organized things well, the algorithms will almost always be self-evident. Data structures, not algorithms, are central to programming.
— Rob Pike

Aşığıda ürün veri tipine örnek bir kaç alan gösterimi mevcuttur.


{
"categories": [
{
"cultureValueList": {
"tr-TR": "Kazak",
"en-US": "Sweater"
},
"id": 1182
}
],
"color": {
"cultureValueList": {
"tr-TR": "Lacivert",
"en-US": "Navy blue"
},
"id": 23
},
"colorTitle": {
"tr-TR": "LACİVERT MELANJ",
"en-US": "NAVY BLUE MELANGE"
},
"gender": {
"cultureValueList": {
"tr-TR": "Erkek",
"en-US": "Male"
},
"id": 1
},
"modelId": 4034620,
"modelTitle": {
"tr-TR": "Kazak",
"en-US": "Sweater"
},
"optionId": 1000566,
"ozelKod1": "0W0794Z8",
"products": [
{
"productId": 1234,
"productSKUs": [
{
"barcode": 1111111111111,
"productOriginId": 1,
"productOriginName": "TURKEY"
}
],
"size": {
"cultureValueList": {
"tr-TR": "3XL",
"en-US": "3XL"
},
"id": 5430
}
},
{
"productId": 1235,
"productSKUs": [
{
"barcode": 1111111111112,
"productOriginId": 0,
"productOriginName": "TURKEY"
}
],
"size": {
"cultureValueList": {
"tr-TR": "2XL",
"en-US": "2XL"
},
"id": 5874
}
}
],
"regionOptionPrice": {
"1": {
"changedDate": "datetime",
"currencySymbol": "TL",
"discountRatio": 0,
"firstPrice": 50.99,
"optionPrice": 50.99
},
"2": {
"changedDate": "datetime",
"currencySymbol": "USD",
"discountRatio": 0,
"firstPrice": 10.99,
"optionPrice": 10.99
}
}
}

Application katmanında Couchbase N1QL syntax’ının bize sağlamış olduğu “dot notation” ile sadece requestten bize gelen ilgili culture ve ilgili country code bilgilerine göre application katmanının ihtiyaç duyduğu tüm bilgiyi kolaylıkla dönülebilen endpoint’ler geliştirdik. Örnek sorgu ve çıktısı aşağıdaki gibidir. Örnek culture “tr-TR” bazlı sorgu;

SELECT RAW { 
'genderId': gender.id,
'gender': gender.cultureValueList.`tr-TR`,
'modelId': modelId,
'ozelKod1': ozelKod1,
'productDescription': productDescription.`tr-TR`,
'currencyId': regionOptionPrice.`1`.currencyId,
'countryId': regionOptionPrice.`1`.countryId,
'currencySymbol': regionOptionPrice.`1`.currencySymbol,
'firstPrice': regionOptionPrice.`1`.firstPrice,
'price': regionOptionPrice.`1`.optionPrice
}
FROM `options-bucket` USE KEYS ['option::123456']
SQL Response: 
[
{
"countryId": 1,
"currencyId": 1,
"currencySymbol": "TL",
"firstPrice": 99.99,
"gender": "Kadın",
"genderId": 2,
"modelId": 6257975,
"ozelKod1": "W2I204Z8",
"price": 99.99,
"productDescription": "Kazak"
}
]

Örnek culture “en-US” bazlı sorgu;

SELECT RAW { 
'genderId': gender.id,
'gender': gender.cultureValueList.`en-US`,
'modelId': modelId,
'ozelKod1': ozelKod1,
'productDescription': productDescription.`en-US`,
'currencyId': regionOptionPrice.`2`.currencyId,
'countryId': regionOptionPrice.`2`.countryId,
'currencySymbol': regionOptionPrice.`2`.currencySymbol,
'firstPrice': regionOptionPrice.`2`.firstPrice,
'price': regionOptionPrice.`2`.optionPrice
}
FROM `options-bucket` USE KEYS ['option::123456']
SQL Response:
[
{
"countryId": 2,
"currencyId": 2,
"currencySymbol": "USD",
"firstPrice": 20.99,
"gender": "Female",
"genderId": 2,
"modelId": 6257975,
"ozelKod1": "W2I204Z8",
"price": 99.99,
"productDescription": "Sweater"
}
]

Veri yapısı, veriye erişim ve database teknolojisi olarak Couchbase kullanımına kararımızı verdikten sonra ki bir diğer önemli adımımız event driven bir yapıda büyük mimaride verinin akış flow’unu belirlemek ve burada mimari yapıya göre teknolojiyi seçmek oldu.

Daha öncesinde single instance in memory yönettiğimiz Inventory Service (legacy system) içerisinde bulunan product domaini için diğer tüm dış domainlerle event driven haberleşebilmesi için kafka üzerinden veriye ait birçok event’in geldiği ve product domaininin veri tabanınının (Couchbase) beslendiği yapıyı kurguladık.

Product Domain Modernization

Modernizasyon sürecinde 5 yeni (.net core) servis geliştirildi. Yukarıdaki grafiği özetle yorumlayacak olursak; verinin hayatı PIM(Product Information Management) sisteminde zenginleştirme ve temel bilgileri girilerek başlıyor. Buradan E-Ticaret için tanımlanmış olan (“EPIM Broker Api”) channel ile satışına onay verilen ürünler gönderiliyor, e-ticaret contex’inde bulunan “Product Aggregation” service ile validasyonu tamamlanan ürünler outbox pattern kullanılarak ilgili Kafka topic’lerine publish ediliyor, burada RabbitMQ yerine Kafka’yı kullanmamızın en büyük sebeplerinden biri veririn publish edildiği sıra ile kayıt olmasının önemi ve ileride bu topicleri başka domainlerin de bind olarak consume edebilmesi ihtiyacıydı, RabbitMQ fanout exchange’i extension yardımı ile sıralı şekilde consume edilmesini sağlayıp kullanmak yerine daha performanslı ve kullanımı basit olduğu için Kafka tercihinde bulunduk.

Kafka topiclerine gelen eventler bir ürünün satışa çıkmasına karar veren status event’i de dahil olmak üzere birçok event tarafından oluşturuldu. Kafka’dan gelen event’ler ile hem catalog domaini hem de product domaini data storage’ları eventler ile beslenebilir hale getirildi. Böylelikle her iki domain price değişiklikleri gibi önemli işlemlerde near real time olarak ürün detay ve listelemede fark oluşumunun önüne geçilecek şekilde consume edebilir hale getirildi.

Eski yapıda ürünler SQL tablolarına yazıldıktan sonra belirli aralıklarla hem legacy inventory service memory katmanına çıkarılmaya çalışılıyor hem de aynı zamanda catalog domain ’inde belirli joblarla schedule halde SQL tabloları taranarak catalog domain’ i Elasticsearch tarafına çıkarılıyordu. Monolit bir veri tabanı olan MSSQL diğer domainlerde CPU yükselmelerine neden olarak ürün yükleme sürecinde bazen payment, delivery gibi domainlerimizde kesintilere sebebiyet verebiliyordu. Buna çözüm olarak her iki domain ’de de yeni gelen ürünler ve değişiklik içeren ürünler için monolit database’e erişim tamamen kapatılarak Kafka’dan eventleri consume ederek event base çalışır duruma getirildi.

Bu noktada da en büyük zorluğumuz dağıtık veri tabanlarında bulunan ve Kafka üzerinden event’lerle oluşturulan ürün bilgilerinin tüm domainler için database’lerde tutarlı(consistency) olması sorunu oldu. Bu konuda çözüm olarak Kafka’dan domain bazlı consumer gruplarının inbox pattern ile birlikte Kafka retry strategy ’lerini uygulaması ve hatalı kayıtları dead-letter topiclerinden consume ederek domain bazlı repair eventleri oluşturarak hata alınan event tipinin PIM tarafından güncel bilgiler ile tekrar publish edilmesi mekanizmalarının geliştirilmeleri oldu.

Modernize ettiğimiz product domain’inde üçüncü aşamada clientlara hizmet verecek olan API’lerin yazılması kısmını devreye soktuk.

Service Aggregator Pattern

Culture ve country bazlı verileri Kafka ’dan okuyup Couchbase kaydetme işlemlerimizden sonra “Product Service” gelen request’e göre içerisinde barındırdığı business’a göre prepare edilmiş Couchbase query’leri genarete ederek client’ların taleplerini karşılayabilir hale getirildi. (CQRS, Decorator pattern) “Product Stock Aggregator Serivce” Service Aggregator Pattern’e uygun olarak product ve stock domainlerine ihtiyaç duyulan yerlerde client’lara “Product Stock Aggregator Service” üzerinden isteklerin karşılanması sağlandı.

Tüm bu yeni servislerimizde http standartlarını karşılamak ve domain bazlı code tekrarlarını önlemek adına NuGet client paketleri geliştirilerek kullanımı standartlaştırıldı.

Modernize edilen servisler diğer domainler de flag ’li bir şekilde devreye alımı gerçekleştirildi. Bu yöntem ile test süreçlerinden gözden kaçan aksi bir durum yaşandığında kararlı halde çalışan eski service’lere istekler yönlendirilerek risk minimize edilmiş oldu. Tekrar yeni sisteme alım süreçlerinde yeni geliştirilen migration tool’umuz max 10 dk içerisinde tüm SQL tablolarını tarayarak tüm verileri Couchbase ’e aktarabildi.

Flag Management New Service

Product modernizasyonu yeni teknolojileri deneyimlediğimiz, gelişemeye hızla devam eden ve diğer tüm domainleri modernize etmemizde domino taşı etkisi gösteren keyifli bir proje oldu. İçerisinde kullandığımız birçok derinlemesine teknik yöntemleri ve diğer modernizasyon süreçleri hakkındaki bilgileri anlattığımız diğer makalelerimizde görüşmek üzere. :)

--

--