Software · May 13, 2026 · 11 dk okuma
Monolith'ten Mikroservislere: Gerçek Bir Sistemi Ne Zaman ve Nasıl Parçalamalı?
Mikroservis mimarisine geçmek bir modernleşme hedefi değil, belirli darboğazlara verilen teknik bir karardır. Modular Monolith, service boundaries, distributed transactions ve operasyonel maliyetler üzerinden doğru zamanı ve geçiş yöntemini inceliyoruz.

Monolith bir sistemi mikroservislere ayırmak, kodu farklı repository'lere taşımak veya uygulamayı birkaç container içinde çalıştırmak değildir. Asıl değişiklik, sistemin hangi parçalarının bağımsız geliştirilebileceğini, deploy edilebileceğini ve kendi verilerinden sorumlu olabileceğini yeniden belirlemektir. Bu bağımsızlık gerçek bir ihtiyacı karşılamıyorsa mikroservis mimarisi mevcut karmaşıklığı ortadan kaldırmaz; karmaşıklığın bir bölümünü network üzerine taşır.
Bu kararın bedeli yalnızca geliştirme süresiyle ölçülmez. Monolith içinde bir method call ile tamamlanan işlem, servisler ayrıldığında network failure, timeout, retry ve data consistency problemleriyle karşılaşabilir. Tek bir database transaction içinde korunan iş kuralları artık birden fazla servisin koordinasyonunu gerektirebilir. Dolayısıyla doğru soru sistemin mikroservislere bölünüp bölünemeyeceği değil, bu bölünmenin getirdiği bağımsızlığın operasyonel maliyeti karşılayıp karşılamadığıdır.
Monolith sorunu her zaman mimari değildir
Monolith, uygulamanın temel olarak tek bir deployment unit şeklinde çalıştığı mimaridir. Bu tanım, kodun mutlaka düzensiz olduğunu veya sistemin ölçeklenemeyeceğini söylemez. İyi tasarlanmış bir monolith içinde farklı domain alanları açık interface'lerle ayrılabilir, bağımlılıklar kontrol edilebilir ve uygulamanın birden fazla instance'ı load balancer arkasında çalıştırılabilir.
Monolith hakkında sık duyulan açıklama, uygulama büyüdükçe kodun yönetilemez hale geldiği ve bu nedenle mikroservislere geçilmesi gerektiğidir. Bu açıklama kritik bir mekanizmayı kaçırır: Kodun büyüklüğü ile deployment bağımsızlığına duyulan ihtiyaç aynı şey değildir. Binlerce sınıf içeren bir uygulama iyi tanımlanmış module boundaries sayesinde yönetilebilirken daha küçük bir sistem, birbirinden bağımsız ölçeklenmesi gereken workload'lar nedeniyle servis ayrımına ihtiyaç duyabilir.
Örneğin bir e-ticaret uygulamasında katalog görüntüleme işlemleri yoğun olabilirken sipariş oluşturma daha düşük hacimli fakat daha hassas bir süreç olabilir. Bütün uygulamayı birlikte ölçeklendirmek gereksiz kaynak tüketimine yol açıyorsa bağımsız scaling anlamlı hale gelir. Ancak aynı sistem birkaç application instance ile beklenen yükü karşılıyorsa mikroservis geçişinin performans açısından açık bir getirisi bulunmayabilir.
Deployment sıklığı da önemlidir. Katalog ekibi küçük bir değişiklik için ödeme sisteminin release takvimini bekliyorsa ortak deployment unit organizasyonel darboğaza dönüşmüş olabilir. Buna karşılık tek bir ekip bütün uygulamayı yönetiyor, değişiklikler birlikte test ediliyor ve release süreci sorunsuz ilerliyorsa servis ayrımı yeni koordinasyon yükü yaratabilir.
Burada ilk tercih edilmesi gereken seçenek çoğu zaman Modular Monolith'tir. Modular Monolith, tek deployment unit içinde çalışan fakat domain alanlarını açık module boundaries ile ayıran mimaridir. Amaç ileride her module'ü mutlaka servise dönüştürmek değildir. Amaç, bağımlılıkları kontrol ederek sistemin anlaşılabilir ve değiştirilebilir kalmasını sağlamaktır.
Bir Modular Monolith içinde Order module'ünün Inventory module'ünün database tablolarını doğrudan değiştirmesine izin verilmez. Order, Inventory'nin sunduğu interface üzerinden işlem yapar. Böylece iki module aynı process içinde çalışsa bile aralarındaki ilişki açık kalır.
Bu düzenleme dağıtık sistem maliyeti oluşturmadan önemli bir mimari kazanım sağlar. Method call hâlâ process içinde gerçekleşir; network timeout veya service discovery problemi ortaya çıkmaz. Aynı database kullanılabilir, ancak tabloların ownership kuralları korunur.
Service boundaries koddan değil domain'den çıkar
Mikroservislere geçişte en zor karar kaç servis oluşturulacağı değil, service boundaries'in nereden geçirileceğidir. Yanlış sınırlar seçildiğinde sistem birbirinden bağımsız servislerden oluşmaz. Birbirini sürekli çağırmak zorunda kalan deployment unit'lerden oluşur.
Bu noktada Domain-Driven Design içindeki Bounded Context kavramı işe yarar. Bounded Context, belirli bir domain modelinin ve bu modelde kullanılan kavramların tutarlı anlam taşıdığı sınırdır. Aynı kelime farklı context'lerde farklı kurallara sahip olabilir.
Örneğin Product kavramı Catalog context'inde açıklama, kategori ve görsellerle ilgilidir. Pricing context'inde fiyatlandırma kuralları, Inventory context'inde ise stok hareketleri önem kazanır. Bu alanların hepsini aynı Product entity'sine bağlamak, başlangıçta kod tekrarını azaltıyor gibi görünse de değişikliklerin birbirine yayılmasına neden olabilir.
Yine de her Bounded Context otomatik olarak ayrı microservice olmak zorunda değildir. Bounded Context domain modelinin sınırını belirler; service boundary ise buna ek olarak deployment, ownership ve operasyonel bağımsızlığı belirler. Birden fazla Bounded Context aynı servis içinde bulunabilir. Özellikle küçük ekiplerde bu tercih daha düşük maliyetlidir.
Service boundaries belirlerken yalnızca entity listesine bakmak yanıltıcıdır. OrderService, ProductService ve CustomerService isimleri domain'i açıklıyor gibi görünse de gerçek iş akışlarını incelemeden doğru ayrımı garanti etmez.
Daha güvenilir yöntem, sistemin hangi değişiklikleri birlikte yapmak zorunda kaldığını incelemektir. Bir fiyatlandırma kuralı değiştiğinde sipariş, katalog ve kampanya kodlarının tamamı değişiyorsa sınırlar doğru kurulmamış olabilir. Buna karşılık Inventory kendi kurallarını değiştirdiğinde Order'ın yalnızca mevcut contract üzerinden çalışmaya devam etmesi güçlü bir ayrım işaretidir.
Data ownership burada belirleyicidir. Database-per-Service yaklaşımında her servis kendi verisinin sahibidir. Bu, her servisin fiziksel olarak ayrı database server kullanması gerektiği anlamına gelmez. Kritik kural, başka bir servisin sahip olduğu tablolara doğrudan erişilmemesi ve schema değişikliklerinin servis contract'ını atlamamasıdır.
İki servis aynı database'i kullanıyor ve birbirlerinin tablolarını doğrudan güncelliyorsa deployment ayrılığı gerçek bağımsızlık sağlamaz. Bir schema migration iki servisi aynı anda değiştirmeyi gerektiriyorsa dağıtık monolith oluşmaya başlamıştır.
Service boundary'nin kalitesi, servis sayısından daha değerlidir. Az sayıda fakat kendi iş kurallarından sorumlu servis, çok sayıda ve sürekli birbirini çağıran servisten daha yönetilebilir olabilir.
Sipariş akışı ayrımın gerçek maliyetini gösterir
Bir e-ticaret backend'inde sipariş oluşturma işlemini düşünelim. Sistem başlangıçta tek bir uygulama ve PostgreSQL database üzerinde çalışıyor olsun. Sipariş oluşturulurken sepet doğrulanıyor, stok kontrol ediliyor, ödeme başlatılıyor ve sipariş kaydı güncelleniyor.
Monolith içindeki CreateOrder akışı uygun tasarlandığında Order ve Inventory kayıtlarını aynı database transaction içinde değiştirebilir. Transaction commit edilmeden hata oluşursa ilgili değişiklikler rollback edilir. Ancak harici payment provider çağrısı aynı database transaction tarafından geri alınamaz. Monolith olmak, dış sistemlerle yapılan işlemleri kendiliğinden atomic hale getirmez.
Inventory ayrı bir servise taşındığında Order artık stok tablolarını doğrudan güncelleyemez. Bunun yerine Inventory API üzerinden reservation oluşturur. Böylece eskiden local transaction içinde gerçekleşen iki değişiklik artık iki farklı servisin sorumluluğundadır.
İlk tasarımda OrderService, InventoryService'e synchronous HTTP request gönderebilir. Synchronous communication, çağıran tarafın yanıt bekleyerek ilerlediği iletişim modelidir. Stok uygunluğu gibi anlık karar gerektiren işlemlerde doğal bir seçimdir.
Ancak bu çağrı network failure ihtimali taşır. Inventory işlemi başarıyla tamamlamış fakat response OrderService'e ulaşmamış olabilir. OrderService timeout gördüğünde işlemin başarısız olduğunu kesin olarak bilemez.
Blind retry burada ciddi hata oluşturabilir. Aynı reservation iki kez açılabilir veya aynı ödeme iki kez başlatılabilir. Bu nedenle Idempotency gerekir. Idempotency, aynı mantıksal isteğin tekrar uygulanmasının ilave iş etkisi üretmemesidir. Örneğin orderId ve reservationId birlikte kullanılarak tekrar gelen reservation request'i yeni stok ayırmak yerine önceki sonucu döndürebilir.
Sipariş sürecinde bütün işlemlerin synchronous olması da iyi bir tercih değildir. Sipariş onay e-postası veya analitik event'i gibi işlemler sipariş response'unu bekletmemelidir. Bu noktada asynchronous communication kullanılır: Servis bir event yayımlar ve tüketici işlemi daha sonra gerçekleştirir.
Fakat asynchronous communication kendi güvenilirlik problemini getirir. OrderService önce database'e siparişi yazıp ardından message broker'a OrderCreated event'i gönderiyorsa iki işlem arasında process crash yaşanabilir. Sipariş kaydedilir ama event yayımlanmaz.
Transactional Outbox bu boşluğu kapatır. Order kaydı ile outbox event'i aynı local database transaction içinde yazılır. Ayrı bir publisher, outbox kayıtlarını broker'a gönderir. Böylece transaction commit edildiyse event'in daha sonra yayımlanması mümkün olur.
Bu yöntem exactly-once delivery garantisi vermez. Publisher event'i gönderdikten sonra gönderildi bilgisini kaydetmeden çökerse aynı event tekrar yayımlanabilir. Consumer bu yüzden event ID üzerinden deduplication yapmalı veya işlemini idempotent tasarlamalıdır.
Servisler ayrıldığında distributed transactions konusu da ortaya çıkar. Order, Inventory ve Payment farklı database'lerde işlem yapıyorsa tek bir local ACID transaction bütün süreci kapsamaz. Distributed transaction koordinasyonu teorik olarak mümkün olsa da servislerin kullanılabilirliğini ve bağımsızlığını etkileyebilir.
Bir alternatif Saga Pattern'dir. Saga, birden fazla local transaction'ı iş akışı halinde birleştirir ve başarısızlık durumunda compensation işlemleri çalıştırır.
Örneğin sipariş önce PENDING durumunda oluşturulur. Inventory stok ayırır, Payment ödeme yetkilendirmesi yapar ve Order siparişi CONFIRMED durumuna geçirir. Ödeme yetkilendirmesi başarısız olursa stok reservation'ı iptal edilir ve sipariş CANCELLED olur.
Buradaki compensation, database rollback ile aynı şey değildir. Ödeme gerçekten tahsil edildiyse refund gerekebilir; refund da başarısız olabilir. Bu nedenle Saga tasarımında timeout, tekrar deneme, manuel müdahale ve ara durumlar açıkça modellenmelidir.
Eventual consistency, farklı servislerdeki verilerin anında aynı durumu göstermeyebileceği fakat yeni değişiklikler ve hatalar durulduğunda tutarlı hale gelmesinin beklendiği modeldir. Sipariş onaylanmışken analitik görünümün birkaç saniye geriden gelmesi kabul edilebilir. Aynı tolerans stok rezervasyonunun iki kez yapılmasına uygulanamaz.
Bu ayrım mimari kararın merkezindedir: Hangi bilgiler gecikmeli güncellenebilir, hangi invariants ise işlem sırasında mutlaka korunmalıdır? Güçlü consistency gerektiren kuralların aynı transactional boundary içinde tutulması çoğu zaman daha güvenlidir.
Bu sistemi neden mikroservise ayırmıyorum?
Aynı e-ticaret sisteminin tek bir backend ekibi tarafından geliştirildiğini, deployment sürecinin düzenli çalıştığını ve mevcut database'in yükü karşılayabildiğini varsayalım. Catalog, Order, Inventory ve Payment kodları aynı repository'de bulunuyor; ancak module boundaries belirgin, testler ayrılmış ve bağımlılıklar kontrol altında.
Bu koşullarda sistemi mikroservislere ayırmam. Çünkü çözülmesi gereken somut bir deployment veya ownership darboğazı henüz gösterilmiş değildir.
Mikroservislere geçiş application server sayısını azaltmayabilir. Aksine API Gateway, message broker, service discovery, merkezi log toplama ve distributed tracing gibi bileşenlerin işletilmesini gerektirir.
API Gateway, istemcilerden gelen request'ler için ortak giriş noktası olabilir. Authentication, routing ve rate limiting gibi cross-cutting concerns burada ele alınabilir. Ancak gateway bütün iş kurallarının toplandığı yeni bir monolith'e dönüşmemelidir.
Service discovery, servis instance'larının değişen network adreslerini bulma mekanizmasıdır. Kubernetes gibi ortamlarda platformun sağladığı service abstraction bu ihtiyacı karşılayabilir. Her sistem için ayrı bir discovery product kurmak gerekli değildir.
Servis sayısı arttığında observability de değişir. Monolith içinde tek request log'u üzerinden izlenen işlem, mikroservislerde birden fazla process'e yayılır. Metrics, structured logs ve distributed tracing birlikte kullanılmalıdır. Trace ID'nin servisler arasında taşınması, request'in hangi çağrıda geciktiğini veya başarısız olduğunu görmeyi sağlar.
Burada observability yalnızca dashboard kurmak değildir. Timeout oranlarını, retry davranışını, queue lag'i, başarısız Saga adımlarını ve outbox backlog'unu izlemek gerekir. Aksi halde sistemin hata verdiği bilinir fakat hangi iş işleminin yarım kaldığı anlaşılamaz.
Deployment complexity de gerçek bir maliyettir. Bağımsız deployment yapmak isteyen servislerin backward-compatible API ve event contract'ları olmalıdır. Database migration süreçleri eski ve yeni servis sürümlerinin bir süre birlikte çalışabileceği şekilde tasarlanmalıdır.
Bir servis yeni event formatını yayımladığında diğer servislerin aynı anda deploy edilmesi gerekiyorsa beklenen bağımsızlık elde edilmemiştir. Bu bağımlılıkların kontrolü, container oluşturmak kadar mimari iş gerektirir.
Benim değerlendirmemde ekip yapısı da teknik mimarinin bir parçasıdır. Tek ekip tarafından birlikte değiştirilen domain alanlarını ayrı servislere dağıtmak, çoğu zaman mevcut koordinasyonu network üzerinden yeniden üretir. Buna karşılık ayrı ekiplerin bağımsız release ihtiyacı varsa aynı sınır gerçek bir avantaj sağlayabilir.
Bu nedenle mevcut örnekte ilk yatırımım service extraction değil, Modular Monolith disiplinini güçlendirmek olur. Module ownership kurallarını testlerle korur, doğrudan tablo erişimlerini azaltır ve hangi iş akışlarının gerçekten bağımsız değiştiğini ölçerim.
Geçiş kararı kanıtla verilir
Mikroservislere geçişi tek seferlik büyük bir yeniden yazım olarak planlamam. Önce belirli bir darboğazı tanımlarım: Örneğin Inventory workload'u diğer alanlardan bağımsız scaling gerektiriyor veya farklı ekiplerin release takvimleri sürekli birbirini engelliyor olabilir.
Ardından ayrılacak Bounded Context'in data ownership ve integration contract'larını netleştiririm. Geçiş sırasında iki sistemin aynı veriyi kontrolsüz biçimde güncellemesine izin vermem. Eski monolith ile yeni servis arasında geçici bir integration layer gerekebilir.
Strangler Fig Pattern burada işe yarar. Yeni işlev veya belirli request akışları kademeli olarak yeni servise yönlendirilir; eski uygulamanın ilgili sorumluluğu kontrollü biçimde kaldırılır. Böylece bütün sistemi aynı anda değiştirmek yerine tek bir boundary'nin maliyeti ölçülebilir.
İlk extraction için genellikle transaction bağımlılığı düşük, ownership sınırı belirgin bir alan seçerim. Örneğin katalog içeriğinin yönetimi, sipariş ile ödeme arasındaki sıkı consistency gereksinimlerine kıyasla daha güvenli bir başlangıç olabilir. Yine de katalog ile fiyatlandırma arasındaki gerçek bağımlılıklar incelenmeden karar verilmez.
Geçişin başarısını yalnızca yeni servisin çalışmasıyla ölçmem. Deployment bağımsızlığı gerçekten artmış mı, incident recovery kolaylaşmış mı, latency kabul edilebilir mi, operasyonel yük değişmiş mi ve ekipler contract değişiklikleri yüzünden yeniden birbirini bekliyor mu? Bu soruların yanıtı sonraki extraction kararını belirler.
Synchronous çağrılarda timeout budget açık olmalıdır. Bir request beş servisi sırayla çağırıyorsa toplam latency ve failure probability zincirdeki bağımlılıklardan etkilenir. Retry eklemek tek başına çözüm değildir; retry storm, zaten zorlanan servisin yükünü artırabilir. Circuit breaker, bounded retry ve deadline propagation gibi mekanizmalar ancak çağrıların iş anlamı bilindiğinde doğru yapılandırılır.
Asynchronous iş akışlarında da queue bulunması güvenilirlik garantisi sayılmaz. Consumer idempotency, dead-letter handling, event schema evolution ve gerektiğinde replay stratejisi tasarlanmalıdır. Bir event'in iki kez veya beklenenden geç ulaşması normal bir çalışma koşulu olarak ele alınmalıdır.
Karar için evrensel bir servis sayısı veya trafik eşiği yoktur. On servis fazla, üç servis doğru gibi bir kural teknik gerekçeye dayanmaz. Daha anlamlı eşik, tek deployment unit'in ölçülebilir biçimde bağımsız scaling, ekip ownership veya release hızını engellemeye başladığı noktadır.
Önerim nettir: Mevcut sistemin sınırları belirsizse önce Modular Monolith kurun. Bounded Context'leri ayırın, module contract'larını belirleyin, database ownership'i netleştirin ve bağımlılıkları görünür hale getirin. Ancak bundan sonra somut darboğazı çözecek service boundary'yi çıkarın.
Bu öneri, belirli workload'ların zorunlu izolasyon gerektirdiği, farklı güvenlik veya compliance sınırlarının bulunduğu ya da bağımsız ekiplerin release ihtiyaçlarının tek deployment modelini açıkça yetersiz bıraktığı durumlarda ilk tercih olmaktan çıkar. Böyle koşullarda başlangıçtan itibaren ayrı servisler daha doğru olabilir.
Mikroservis mimarisindeki asıl yetkinlik çok sayıda servis oluşturmak değildir. Hangi bağımlılığın process içinde kalması gerektiğini, hangisinin network sınırını geçmeye değer olduğunu ve bu kararın hangi failure mode'ları yarattığını bilmektir. Gereksiz dağıtık sistem kurmamak da bilinçli bir mimari karardır.
yorumlar
0Henüz yorum yok. İlk notu sen bırak.
Yorum yazmak için giriş yap.
Gmail ile saniyeler içinde, ya da e-posta ile kaydol.