[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"blog-page":3,"blog-posts":16},{"id":4,"title":5,"body":6,"description":7,"extension":8,"links":6,"meta":9,"navigation":10,"path":11,"seo":12,"stem":14,"__hash__":15},"pages\u002Fblog.yml","Blog",null,"Frontend, backend ve mobil tarafında geliştirirken karşılaştığım problemler ve çözümleri üzerine notlarım.","yml",{},true,"\u002Fblog",{"title":5,"description":13},"Nuxt, Vue, React, Node.js ve MongoDB üzerine geliştirme notları.","blog","criFC2foB2oFvzSuBXeZAAdI08xF9Vr5kNxAM7rsoeQ",[17,273,363],{"id":18,"title":19,"author":20,"body":25,"date":263,"description":264,"extension":265,"image":266,"meta":267,"minRead":268,"navigation":10,"path":269,"seo":270,"stem":271,"__hash__":272},"blog\u002Fblog\u002Fmikroservis-event-driven-mimari.md","Velora'da mikroservisleri event bus ile konuşturmak",{"name":21,"description":22,"avatar":23},"Uhut Sancar","Frontend Developer",{"src":24,"alt":21},"\u002Fprofile.jpg",{"type":26,"value":27,"toc":252},"minimark",[28,32,35,40,43,62,69,73,76,91,94,98,105,125,143,146,150,153,174,182,186,200,207,214,221,225,231,235,238,241],[29,30,31],"p",{},"Velora, .NET ile geliştirdiğim mikroservis tabanlı bir e-ticaret altyapısı. Frontend tarafında çalışırken sürekli tükettiğim API'lerin arkasında ne olduğunu uçtan uca görmek istediğim için başladım.",[29,33,34],{},"Projede servisleri ayırmak beklediğimden kolay, onları birbirine bağlamak beklediğimden zor oldu. Asıl mesele servisi bölmek değil; bölündükten sonra aralarındaki iletişimi nasıl kuracağın.",[36,37,39],"h2",{"id":38},"servis-sınırlarını-nereden-çizdim","Servis sınırlarını nereden çizdim",[29,41,42],{},"Çözümde altı servis var: Identity, Catalog, Basket, Order, Payment ve Notification. Bunları teknik katmana göre değil, iş sürecine göre ayırdım. Her servisin kendi veritabanı, kendi modeli ve kendi API'si var.",[29,44,45,46,50,51,50,54,57,58,61],{},"Order tarafında katmanları da ayrı tuttum: ",[47,48,49],"code",{},"OrderService.Api",", ",[47,52,53],{},"OrderService.Application",[47,55,56],{},"OrderService.Domain"," ve ",[47,59,60],{},"OrderService.Infrastructure",". Domain kuralları dışarıya bağımlı olmayan bir yerde duruyor, veri erişimi ve dış entegrasyonlar Infrastructure'a düşüyor.",[29,63,64,65,68],{},"İstemciler servislere doğrudan gitmiyor. Önde Ocelot ile kurduğum bir API Gateway var; yönlendirme kuralları ",[47,66,67],{},"ocelot.json"," içinde tanımlı. Böylece Blazor istemcisi tek bir adres biliyor, arkadaki servis sayısı değişince istemci tarafı etkilenmiyor.",[36,70,72],{"id":71},"neden-senkron-çağrı-değil","Neden senkron çağrı değil",[29,74,75],{},"İlk refleks, sipariş oluşturulduğunda Order servisinin Payment servisini doğrudan çağırması oluyor. Bu çalışır ama iki servisi birbirine bağlar: Payment ayaktayken sipariş alınır, değilken alınamaz. Üstelik yarın araya Notification girdiğinde Order'ın kodunu tekrar açman gerekir.",[29,77,78,79,82,83,86,87,90],{},"Bu yüzden akışı event üzerinden kurdum. Order bir sipariş başlattığında ",[47,80,81],{},"OrderStartedIntegrationEvent"," yayınlıyor, kimin dinlediğini bilmiyor. Payment bu event'i dinleyip ödemeyi işliyor ve sonucuna göre ",[47,84,85],{},"OrderPaymentSuccessIntegrationEvent"," ya da ",[47,88,89],{},"OrderPaymentFailedIntegrationEvent"," yayınlıyor.",[29,92,93],{},"Order da bu sonuç event'lerini dinleyip siparişin durumunu güncelliyor. İki servis birbirinin adresini bilmiyor, sadece ortak bir sözleşmeyi biliyor.",[36,95,97],{"id":96},"event-busı-soyutlamak","Event bus'ı soyutlamak",[29,99,100,101,104],{},"Servislerin doğrudan RabbitMQ ile konuşmasını istemedim. ",[47,102,103],{},"BuildingBlocks"," altında ayrı bir EventBus katmanı kurdum.",[29,106,107,110,111,50,114,50,117,120,121,124],{},[47,108,109],{},"EventBus.Base"," sözleşmeleri tutuyor: ",[47,112,113],{},"IEventBus",[47,115,116],{},"IIntegrationEventHandler",[47,118,119],{},"IntegrationEvent"," ve abonelikleri tutan ",[47,122,123],{},"InMemoryEventBusSubscriptionManager",". Servisler yalnızca bu arayüzleri tanıyor.",[29,126,127,128,57,131,134,135,138,139,142],{},"Somut implementasyonlar ayrı projelerde: ",[47,129,130],{},"EventBus.RabbitMQ",[47,132,133],{},"EventBus.AzureServiceBus",". Hangisinin kullanılacağına ",[47,136,137],{},"EventBusFactory"," karar veriyor; konfigürasyondaki ",[47,140,141],{},"EventBusType"," değerine bakıp ilgili sınıfı üretiyor, varsayılan olarak RabbitMQ dönüyor.",[29,144,145],{},"Bu ayrımın pratik faydası şu: mesajlaşma altyapısını değiştirmek tek bir konfigürasyon satırı. Servis kodlarında tek satır değişmiyor.",[36,147,149],{"id":148},"kuyruk-isimlendirmesi-ve-normalizasyon","Kuyruk isimlendirmesi ve normalizasyon",[29,151,152],{},"Burası ilk denemede kafamı en çok karıştıran yerdi. Event sınıfının adı ile kuyruğun adı aynı olmak zorunda değil ve olmaması gerekiyor.",[29,154,155,158,159,57,162,165,166,169,170,173],{},[47,156,157],{},"ProcessEventName"," metodu, konfigürasyondaki ",[47,160,161],{},"DeleteEventPrefix",[47,163,164],{},"DeleteEventSuffix"," bayraklarına göre sınıf adının başındaki ve sonundaki ekleri temizliyor. ",[47,167,168],{},"GetSubName"," ise temizlenmiş isme ",[47,171,172],{},"SubscriberClientAppName"," değerini ekleyip abonelik adını üretiyor.",[29,175,176,177,181],{},"Bunun sonucu şu: aynı event'i dinleyen iki farklı servis, aynı exchange'e bağlı ama ",[178,179,180],"strong",{},"ayrı"," kuyruklara sahip oluyor. Aynı kuyruğu paylaşsalardı mesajı ikisi de değil, yalnızca biri alırdı. Ödeme başarılı olduğunda hem Order'ın durumu güncellemesi hem Notification'ın bildirim göndermesi gerektiği için bu ayrım kritik.",[36,183,185],{"id":184},"rabbitmq-tarafında-dikkat-ettiklerim","RabbitMQ tarafında dikkat ettiklerim",[29,187,188,189,192,193,86,196,199],{},"Bağlantıyı ",[47,190,191],{},"RabbitMQPersistentConnection"," içinde ayrı tuttum ve Polly ile üstel geri çekilmeli yeniden deneme ekledim. ",[47,194,195],{},"BrokerUnreachableException",[47,197,198],{},"SocketException"," alındığında her denemede bekleme süresi ikiye katlanıyor. Container'lar ayağa kalkarken RabbitMQ genelde uygulamadan sonra hazır oluyor; bu olmadan servis ilk saniyede patlıyordu.",[29,201,202,203,206],{},"Yayınlarken mesajı kalıcı işaretliyorum, kuyrukları ",[47,204,205],{},"durable"," açıyorum. Aksi halde broker yeniden başladığında bekleyen mesajlar kayboluyor.",[29,208,209,210,213],{},"Tüketirken mesajı otomatik onaylatmıyorum. Handler işini bitirdikten sonra ",[47,211,212],{},"BasicAck"," çağırıyorum. Otomatik onayda handler ortasında bir hata olursa mesaj gitmiş oluyor; manuel onayda iş gerçekten tamamlandıysa düşüyor.",[29,215,216,217,220],{},"Event işlenirken her mesaj için ayrı bir DI scope açılıyor, handler o scope içinden çözülüyor. Uzun ömürlü bir servisin içinde ",[47,218,219],{},"DbContext"," gibi scoped bağımlılıkların birbirine karışmaması için gerekli.",[36,222,224],{"id":223},"altyapıyı-ayrı-tutmak","Altyapıyı ayrı tutmak",[29,226,227,230],{},[47,228,229],{},"docker-compose-files"," altında RabbitMQ, Redis, SQL Server ve Consul için ayrı compose dosyaları duruyor. Hepsini tek dosyaya koymak yerine ayırdım; yalnızca RabbitMQ ile uğraştığım bir günde diğerlerini ayağa kaldırmak zorunda kalmıyorum.",[36,232,234],{"id":233},"geriye-kalan","Geriye kalan",[29,236,237],{},"Event driven yaklaşım servisleri gevşek bağlı yapıyor ama karşılığında bir bedel var: akışı tek bir yerden okuyamıyorsun. Bir siparişin neden tamamlanmadığını anlamak için üç servisin logunu birlikte bakmak gerekiyor.",[29,239,240],{},"Bu yüzden sıradaki adımlarım, event'lere korelasyon kimliği taşımak ve tüketici tarafını idempotent hale getirmek. Aynı mesaj iki kez düştüğünde sistemin aynı sonucu üretmesi gerekiyor; şu anki halinde bu garanti değil.",[29,242,243,244,251],{},"Proje devam ediyor, kaynak kodu ",[245,246,250],"a",{"href":247,"rel":248},"https:\u002F\u002Fgithub.com\u002Fuhutsancar\u002FVelora\u002Ftree\u002Fmain\u002FSellingBuddy",[249],"nofollow","GitHub'da"," duruyor.",{"title":253,"searchDepth":254,"depth":254,"links":255},"",2,[256,257,258,259,260,261,262],{"id":38,"depth":254,"text":39},{"id":71,"depth":254,"text":72},{"id":96,"depth":254,"text":97},{"id":148,"depth":254,"text":149},{"id":184,"depth":254,"text":185},{"id":223,"depth":254,"text":224},{"id":233,"depth":254,"text":234},"2026-06-12",".NET ile kurduğum SellingBuddy servislerinde sipariş ve ödeme akışını RabbitMQ üzerinden event driven hale getirirken servis sınırları, integration event'ler ve kuyruk isimlendirmesi üzerine öğrendiklerim.","md","\u002Fimages\u002Fmicroservices-event-driven.svg",{},7,"\u002Fblog\u002Fmikroservis-event-driven-mimari",{"title":19,"description":264},"blog\u002Fmikroservis-event-driven-mimari","JVpJjj0FzZ5uJab6sSrK6Hx4f79zVEJUCnmdXnbk4yc",{"id":274,"title":275,"author":276,"body":278,"date":354,"description":355,"extension":265,"image":356,"meta":357,"minRead":358,"navigation":10,"path":359,"seo":360,"stem":361,"__hash__":362},"blog\u002Fblog\u002Fmongodb-embed-reference.md","MongoDB'de embed mi reference mı?",{"name":21,"description":22,"avatar":277},{"src":24,"alt":21},{"type":26,"value":279,"toc":348},[280,283,289,293,296,299,302,306,309,312,315,319,322,325,335,338,342,345],[29,281,282],{},"MongoDB ile çalışırken en çok zaman kaybettiren karar, veriyi nasıl modelleyeceğim oluyor. İlişkisel veritabanından gelen alışkanlıkla her şeyi ayrı koleksiyonlara bölmek mümkün, ama bu çoğu zaman uygulamanın gerçek sorgu ihtiyacına uymuyor.",[29,284,285,286],{},"Benim çıkış noktam şu: ",[178,287,288],{},"veri modelini varlıkların birbirine olan ilişkisine göre değil, uygulamanın bu veriyi nasıl okuduğuna göre kuruyorum.",[36,290,292],{"id":291},"embedi-ne-zaman-tercih-ediyorum","Embed'i ne zaman tercih ediyorum",[29,294,295],{},"Bir alt veri her zaman ana belgeyle birlikte okunuyorsa ve tek başına sorgulanmıyorsa embed etmek işi kolaylaştırıyor. Bir kullanıcının adres bilgisi, bir ürünün varyant listesi ya da bir ayar objesi bu gruba giriyor.",[29,297,298],{},"Kazanç net: tek sorguda tüm veri geliyor, join benzeri bir işleme gerek kalmıyor.",[29,300,301],{},"Ama embed edilen dizinin sınırsız büyümediğinden emin olmak gerekiyor. Bir belge sürekli büyüyorsa — mesela bir sohbetin tüm mesajları — bu yaklaşım bir noktada belge boyutu limitine ve gereksiz veri transferine dönüşüyor.",[36,303,305],{"id":304},"referenceı-ne-zaman-tercih-ediyorum","Reference'ı ne zaman tercih ediyorum",[29,307,308],{},"Alt veri kendi başına anlamlıysa, tek başına listeleniyorsa, filtreleniyorsa veya sayfa sayfa okunuyorsa ayrı koleksiyona alıyorum.",[29,310,311],{},"Support.io'da mesajları ayrı koleksiyonda tuttum. Bir konuşmanın mesaj sayısının üst sınırı yok, mesajlar tarihe göre sayfalanıyor ve arama yapılabiliyor. Bunları konuşma belgesinin içine gömmek, her konuşma açılışında binlerce mesajı okumak anlamına gelirdi.",[29,313,314],{},"Aynı mantıkla, birden fazla yerden referans verilen ve tek noktadan güncellenmesi gereken veriler de reference olarak duruyor. Aksi halde aynı bilgiyi onlarca belgede güncellemek gerekiyor.",[36,316,318],{"id":317},"index-ve-pagination-tarafı","Index ve pagination tarafı",[29,320,321],{},"Model kararını verdikten sonra sorgu tarafını ihmal etmemek gerekiyor.",[29,323,324],{},"Her sorguda filtrelenen ve sıralanan alanları birlikte düşünüp bileşik index kuruyorum. Sıralama alanı index'in parçası değilse, MongoDB veriyi bellekte sıralamak zorunda kalıyor ve veri büyüdükçe bu maliyet hissediliyor.",[29,326,327,328,331,332,334],{},"Pagination'da ",[47,329,330],{},"skip"," yerine, sıralama yaptığım alan üzerinden imleç mantığıyla ilerlemeyi tercih ediyorum. ",[47,333,330],{}," küçük veri kümelerinde sorun çıkarmıyor ama derin sayfalarda atlanan tüm kayıtları taramak zorunda kalıyor.",[29,336,337],{},"Bir de her sorguda gerçekten ihtiyaç duyduğum alanları seçiyorum. Bir liste ekranında belgenin tamamını çekmek, hem veritabanı hem ağ tarafında bedava değil.",[36,339,341],{"id":340},"karar-verirken-sorduğum-sorular","Karar verirken sorduğum sorular",[29,343,344],{},"Bu veri tek başına sorgulanıyor mu? Sınırsız büyüyor mu? Ana belgeyle her zaman birlikte mi okunuyor? Birden fazla yerden mi güncelleniyor?",[29,346,347],{},"Bu dört sorunun cevabı, çoğu durumda kararı benim yerime veriyor.",{"title":253,"searchDepth":254,"depth":254,"links":349},[350,351,352,353],{"id":291,"depth":254,"text":292},{"id":304,"depth":254,"text":305},{"id":317,"depth":254,"text":318},{"id":340,"depth":254,"text":341},"2026-04-28","Veri modelini uygulamanın sorgu şekline göre kurmak, embed ve reference arasındaki seçim ve bu kararın pagination ile index performansına etkisi.","\u002Fimages\u002FMongoDB_Logo.svg.webp",{},6,"\u002Fblog\u002Fmongodb-embed-reference",{"title":275,"description":355},"blog\u002Fmongodb-embed-reference","WUhWQWvXOiDf8fP4MCsJlq78QsCr8ENUr4iTL6GA7I4",{"id":364,"title":365,"author":366,"body":368,"date":433,"description":434,"extension":265,"image":435,"meta":436,"minRead":358,"navigation":10,"path":437,"seo":438,"stem":439,"__hash__":440},"blog\u002Fblog\u002Fsocket-io-canli-sohbet.md","Support.io'da Socket.IO ile canlı sohbet altyapısı",{"name":21,"description":22,"avatar":367},{"src":24,"alt":21},{"type":26,"value":369,"toc":426},[370,373,376,380,383,386,390,393,396,400,403,406,409,413,416,419,423],[29,371,372],{},"Support.io, bir web sitesine tek satır kodla eklenen SaaS bir canlı destek sistemi. Ziyaretçi widget üzerinden yazıyor, destek ekibi yönetici panelinden yanıtlıyor, aynı panelden birden fazla site yönetilebiliyor.",[29,374,375],{},"Gerçek zamanlı tarafı Socket.IO ile kurdum. Geliştirirken en çok uğraştığım şey mesajın iletilmesi değil; bağlantı koptuğunda, sekme arka plana atıldığında ya da aynı kullanıcı iki cihazdan bağlandığında sistemin tutarlı kalmasıydı.",[36,377,379],{"id":378},"oda-yapısı","Oda yapısı",[29,381,382],{},"Her konuşma için ayrı bir oda kullandım. Ziyaretçi bağlandığında kendi konuşma odasına katılıyor; destek ekibindeki kullanıcı ise ait olduğu sitenin tüm konuşmalarını dinleyebilecek şekilde ek bir odaya alınıyor.",[29,384,385],{},"Bu ayrım önemliydi çünkü bir ziyaretçinin başka bir ziyaretçinin mesajını görmemesi gerekiyor. Yayını \"herkese gönder, istemcide filtrele\" mantığıyla yapmak kolay görünüyor ama güvenlik açısından kabul edilebilir değil.",[36,387,389],{"id":388},"kimlik-doğrulama","Kimlik doğrulama",[29,391,392],{},"Socket bağlantısını kurarken JWT'yi handshake aşamasında doğruluyorum. Token geçersizse bağlantı hiç kurulmuyor.",[29,394,395],{},"Destek ekibi tarafında ek bir kontrol daha var: kullanıcının katılmak istediği odanın, gerçekten yetkili olduğu siteye ait olup olmadığını sunucuda kontrol ediyorum. İstemciden gelen oda bilgisine güvenmek, panelin tamamını açık hale getirirdi.",[36,397,399],{"id":398},"mesaj-sırası-ve-yeniden-bağlanma","Mesaj sırası ve yeniden bağlanma",[29,401,402],{},"Socket bağlantısı kopup yeniden kurulduğunda, kopma süresince gönderilen mesajlar istemciye ulaşmıyor. Bunu çözmek için mesajları yalnızca socket üzerinden akıtmak yerine kalıcı olarak veritabanına yazıyorum; istemci yeniden bağlandığında elindeki son mesajın zaman damgasından sonrasını REST üzerinden çekiyor.",[29,404,405],{},"Yani socket \"anlık iletim\" için, veritabanı ise \"gerçeğin kaynağı\" olarak çalışıyor. Bu ayrımı baştan kurmak, sonradan ortaya çıkan pek çok tutarsızlığı engelledi.",[29,407,408],{},"Kullanıcı deneyimi tarafında ise mesajı gönderirken hemen ekranda gösterip, sunucudan onay geldiğinde durumunu güncelliyorum. Onay gelmezse mesaj \"gönderilemedi\" durumunda kalıyor ve tekrar denenebiliyor.",[36,410,412],{"id":411},"widget-tarafındaki-kısıtlar","Widget tarafındaki kısıtlar",[29,414,415],{},"Widget başka birinin sitesinde çalıştığı için sayfanın stillerinden etkilenmemesi gerekiyordu. Stil izolasyonu ve z-index yönetimi, gerçek zamanlı taraftan daha fazla vaktimi aldı.",[29,417,418],{},"Mobil tarafta klavye açıldığında sohbet alanının kayması, safe area ve viewport yüksekliği gibi konular da ayrı bir başlıktı. Bir widget'ın masaüstünde düzgün görünmesi, mobilde çalıştığı anlamına gelmiyor.",[36,420,422],{"id":421},"çıkardığım-ders","Çıkardığım ders",[29,424,425],{},"Gerçek zamanlı bir özellik geliştirirken asıl iş, mesajı iletmek değil; bağlantının koptuğu, geciktiği ya da iki kez kurulduğu durumları planlamak. Bu senaryoları sonradan eklemeye çalışmak, baştan tasarlamaktan çok daha zor oluyor.",{"title":253,"searchDepth":254,"depth":254,"links":427},[428,429,430,431,432],{"id":378,"depth":254,"text":379},{"id":388,"depth":254,"text":389},{"id":398,"depth":254,"text":399},{"id":411,"depth":254,"text":412},{"id":421,"depth":254,"text":422},"2026-03-10","Tek satır kodla siteye gömülen bir canlı destek sistemini geliştirirken oda yönetimi, yeniden bağlanma ve mesaj sırası konusunda öğrendiklerim.","\u002Fimages\u002Fprojects-1.avif",{},"\u002Fblog\u002Fsocket-io-canli-sohbet",{"title":365,"description":434},"blog\u002Fsocket-io-canli-sohbet","qThLXTNZWAHPEV5NSWZ3kS2BV-nba05u9aR2baM8_NU"]