Fatih Işık ·

Perakende Sektöründe Offline-First Mimariye Geçiş: Neden ve Nasıl?

Giriş

Her şey, yoğun kampanya dönemlerinde mağazalardan gelen performans şikayetleriyle başladı. İlk refleks olarak kodumuza baktık; mikroservislerimize ve web uygulamamıza Application Insights entegre ettik. Darboğazları tespit edip kod tarafında ciddi optimizasyonlar yaptık. Ancak şikayetler kesilmedi.

Veriyi daha derinlemesine incelemeye karar verdik. İstatistikleri kasa, mağaza ve işlem tipi bazında grupladığımızda ilginç bir tabloyla karşılaştık: Sorun kodumuzda değil, üzerinde koştuğu “yolda” idi.

Problem: Heterojen ve Güvenilmez Ağlar

Mağazalarımızda Fiber’den VDSL’e, ADSL’den yedek mobil internete (4G) kadar çok çeşitli ve dalgalı bir bağlantı altyapısı vardı. En iyi kodu da yazsak, fiziksel hattın limitlerine takılıyorduk.

Özellikle kampanya günlerinde artan trafikle birlikte, düşük bant genişliğine sahip mağazalarda paket kayıpları ve yüksek gecikme (latency) süreleri yaşanıyordu. Bu durum, kasada bekleyen müşteri için “sistemin donması”, kasiyer için ise stres demekti. Anladık ki, sorunu sunucu tarafında (backend) çözemeyiz; sorunu kaynağında, yani kasada (edge) çözmeliyiz.

Çözüm Arayışı ve Karar Anı

Sorunu tespit ettikten sonra “Ne yapabiliriz?” sorusuyla beyin fırtınasına başladık. Masada üç ana fikir vardı:

  1. Ayrı Bir Offline Uygulama Yazmak: En kolayı gibi görünse de, iki farklı kod tabanını (Online ve Offline) yönetmek ve iş kurallarını senkronize tutmak sürdürülebilirlik açısından bir kabustu. Bunu eledik.
  2. Mikroservisleri Lokale Taşımak (Docker): Mevcut mikroservislerimizi Docker konteynerleri içinde kasalarda çalıştırmayı düşündük. Ancak binlerce kasadaki donanım kısıtları ve dağıtık sistemin yönetim zorluğu (orchestration) bizi korkuttu. Bu, sorunu çözmek yerine şekil değiştirtmek olurdu.
  3. Local Replica Server (Kazanan Fikir): Benim öncülüğünü yaptığım bu fikir, “İş mantığını (Business Logic) veriden ayıralım” prensibine dayanıyordu. Eğer iş mantığını soyutlayabilirsek, aynı kodu hem buluttaki SQL Server ile hem de kasadaki SQLite ile konuşturabilirdik.

Kararımızı verdik: Yeni bir uygulama yazmayacaktık. Mevcut mimarimizi, veri kaynağından bağımsız çalışabilen ve lokalde hafif bir “Replica API” barındıran bir yapıya evriltecektik.

Teknoloji Seçimi: Neden SQLite?

Dil ve framework konusunda elimiz rahattı; mikroservislerimiz zaten .NET ile yazılmıştı, bu yüzden ekosistemi değiştirmeye gerek yoktu. Ancak asıl soru şuydu: Veriyi nerede tutacağız?

Aradığımız veritabanının “Gömülü” (Embedded) olması şarttı. Binlerce kasaya ayrı bir SQL Server kurulumu yapmak operasyonel bir intihar olurdu. Bu kriterle yola çıkarak Couchbase Lite, LiteDB ve SQLite gibi alternatifleri masaya yatırdık.

Başta Couchbase Lite bize çok cazip geldi çünkü merkezde zaten Couchbase kullanıyorduk. Ancak saha gerçekleri yüzümüze çarptı: Bazı eski kasa donanımları 32-bit mimarideydi ve Couchbase Lite’ın 32-bit desteği yoktu. Bu bizi daha yalın, daha evrensel ve her yerde çalışan bir çözüme itti: SQLite.

Ancak SQLite ilişkisel (Relational) bir veritabanıydı, bizim merkezi yapımız ise NoSQL’di. Her yeni özellikte binlerce kasada şema güncellemesi (Migration) yapmak istemiyorduk. Tam bu noktada SQLite’ın JSON desteği imdadımıza yetişti.

“Neden SQLite’ı bir NoSQL veritabanı gibi kullanmıyoruz?” dedik. Veriyi JSON olarak saklayıp, sorgulamamız gereken alanlar için “Generated Columns” üzerinden indeksler oluşturduk. Böylece hem şema bağımsızlığımızı (Schema-less) koruduk hem de SQLite’ın hızından ve güvenilirliğinden faydalandık. Generic bir koleksiyon yapısı kurarak geliştirmeye başladık.

Mimari Detay: İş Mantığının Soyutlanması

En büyük korkumuz kod tekrarıydı. Hem merkezi mikroservislerde hem de kasadaki offline uygulamada aynı satış kurallarını (indirim, kampanya, vergi vb.) tekrar tekrar yazmak istemiyorduk.

Çözüm olarak Repository Pattern’i merkeze aldık. İş mantığı katmanını (Business Layer), verinin nereden geldiğinden (SQL Server, Couchbase veya SQLite) tamamen izole ettik. İş akışımız sadece “Bana şu ürünü getir” diyordu; arka planda hangi veritabanına gidildiği onun umurunda değildi. Bu sayede iş mantığını ortak bir NuGet paketine dönüştürdük ve her iki tarafta da %100 kod paylaşımı sağladık.

Veri Senkronizasyonu: Nats JetStream Gücü

Veriyi kasaya indirmek (Sync Down) işin en zor kısmıydı. Binlerce kasanın sürekli merkeze “Değişiklik var mı?” diye sorması (Polling) sunucularımızı kilitlerdi.

Burada Event-Driven (Olay Güdümlü) bir yapı kurduk ve Nats JetStream teknolojisini kullandık. Merkezde bir ürün fiyatı değiştiğinde, JetStream üzerinden ilgili kasalara bir “Event” fırlatılıyor.

Kasa tarafındaki Offline Server’da çalışan bir Background Worker, bu mesajları dinliyor ve lokal SQLite veritabanını güncelliyor. JetStream’in “Durable” (Kalıcı) yapısı sayesinde, kasa o an kapalı olsa veya interneti olmasa bile mesaj kaybolmuyor; kasa açıldığı anda sıradaki mesajları işleyip güncel hale geliyor.

Kasadan Merkeze Veri Akışı (Sync Up)

Veriyi indirmek kadar, oluşan satışı merkeze göndermek de kritikti. Burada Quartz.NET kütüphanesini kullanarak koleksiyon bazlı işler (Jobs) tanımladık.

Bir satış tamamlandığında, veri önce güvenli liman olan lokal SQLite’a yazılıyor. Hemen ardından arka planda tetiklenen bir Job, bu veriyi alıp merkeze iletiyor. Eğer o an internet yoksa, Job bir sonraki deneme zamanını bekliyor. Böylece satış anında müşteriyi bekletmeden, veriyi “sıcağı sıcağına” merkeze taşıyan asenkron bir yapı kurmuş olduk.

Elbette her ihtimale karşı bir “Fallback” mekanizması da kurduk: Offline Server her açılışta kritik sabit verileri (Master Data) kontrol edip eksik varsa tamamlıyor.

Sonuç ve Kazanımlar

Bu mimari dönüşümün ardından mağazalardan gelen “Sistem dondu” şikayetleri bıçak gibi kesildi. Kasa işlemleri, internet hızından bağımsız olarak milisaniyeler seviyesine indi.

Ancak en büyük kazanımımız sadece hız olmadı; esneklik kazandık. Kurduğumuz bu “Offline-First” altyapı, şu an geliştirmekte olduğumuz Self-Checkout projesinin de omurgasını oluşturuyor. Müşterilere, ağ kesintilerinden etkilenmeyen, hızlı ve akıcı bir ödeme deneyimi sunabiliyoruz.

Bir teknik lider olarak bu süreçten çıkardığım en büyük ders şuydu: Bazen en iyi çözüm, en yeni teknolojiyi kullanmak değil (Docker vs.), eldeki problemi en basit ve en sağlam şekilde çözen mimariyi (Local Replica) kurmaktır.