Monolit mi Mikroservis mi? Kurumsal Java İçin Mimari Kararı
Mikroservis bir kalite göstergesi değil, dağıtım ve organizasyon kararıdır. Kurumsal Java projelerinde monolit–mikroservis kararını ekip ölçeği, veri tutarlılığı ve operasyon maliyeti üzerinden ele alıyor; Spring Modulith ile modüler monolit kurulumunu kod örnekleriyle gösteriyoruz.
Kurumsal bir Java projesine başlarken en sık duyduğumuz soru şu: "Mikroservis mi yapalım, monolit mi?" Soru doğru, ama genellikle yanlış zamanda ve yanlış gerekçeyle soruluyor. Mikroservis bir kalite göstergesi değil, bir dağıtım ve organizasyon kararıdır; bedeli de çoğu zaman ilk sprintlerde değil, ikinci yılda ödenir.
Bu yazıda kararı hangi kriterlerle vermek gerektiğini, iki yaklaşımın gerçek maliyetlerini ve çoğu kurumsal proje için önerdiğimiz başlangıç noktasını — modüler monoliti — Spring Boot üzerinde somut kod örnekleriyle anlatıyoruz.
Önce tanımları netleştirelim
Tartışmanın büyük kısmı, üç farklı şeyin aynı kelimeyle anılmasından kaynaklanıyor.
Monolit, tek bir dağıtım birimi olarak çalışan uygulamadır: tek süreç, genellikle tek veritabanı. "Spagetti kod" ile eş anlamlı değildir; dağınıklık mimarinin değil disiplinin sonucudur.
Modüler monolit, yine tek dağıtım birimidir; farkı, modül sınırlarının kodda ve derleme zamanında zorlanmasıdır. Sipariş modülü stok modülünün iç sınıflarına erişemez; yalnızca açık API'sini çağırır ya da yayınladığı olayları dinler. Her modül kendi tablolarının sahibidir.
Mikroservis, bağımsız dağıtılan, kendi verisine sahip ve diğerleriyle ağ üzerinden konuşan servislerdir.
Kritik nokta: modüler monolit ile mikroservis arasındaki fark kod kalitesi değil, dağıtım birimi ve süreç sınırıdır. Mantıksal sınırları doğru çizmişseniz, ikisi arasında geçiş bir mimari devrim değil, adaptör yazma işidir. Sınırları çizmemişseniz, mikroservise geçmek dağınıklığı ağ üzerine taşımaktan ibaret kalır.
Mikroservisin gerçek maliyeti: dağıtık sistem vergisi
Süreç sınırını ağ sınırına çevirdiğiniz anda dört fatura birden gelir.
1. Ağ ve kısmi arıza
Aynı süreçteki bir metot çağrısı nanosaniyeler sürer ve ya çalışır ya da exception fırlatır. Ağ üzerinden aynı çağrı milisaniyeler sürer ve üçüncü bir sonucu vardır: cevap gelmedi. Timeout, retry, circuit breaker, idempotency — bunların hiçbiri isteğe bağlı değildir; hepsi tasarlanmak, test edilmek ve izlenmek zorundadır.
2. Veri tutarlılığı
Tek veritabanında "sipariş oluştur + stok düş + fatura kes" tek transaction'dır; ya hepsi olur ya hiçbiri. Üç ayrı serviste bu üç adım artık bir sagadır: her adımın telafi (compensation) işlemi yazılır, "sipariş oluştu ama stok düşmedi" durumu için izleme ve müdahale süreci kurulur. Finans ve stok gibi güçlü tutarlılık gerektiren alanlarda bu, en pahalı kalemdir.
3. Operasyon
Her servis için ayrı CI/CD hattı, konfigürasyon ve gizli anahtar yönetimi, servis keşfi, dağıtık izleme (tracing), log korelasyonu, nöbet planı. Bu altyapı yoksa mikroservis mimarisi değil, izlenemeyen bir arıza ağı kurmuş olursunuz.
4. Organizasyon
Conway yasası tersine de işler: 6 kişilik bir ekip 25 servis işlettiğinde her geliştirici 4 servisin sahibidir ve bağımsız dağıtım avantajı kâğıt üzerinde kalır. Mikroservisin en büyük getirisi ekiplerin birbirini beklemeden yayına çıkmasıdır; tek ekip için bu getiri yoktur.
Monolitin gerçek maliyeti
Monolit ücretsiz değildir; ama faturası farklı kalemlerdedir.
Toplu ölçekleme: Rapor üretimi CPU yerken sipariş API'si de aynı süreçte yavaşlar. Çözüm çoğu zaman ayrı servis değil, aynı kod tabanından ayağa kalkan bir worker profilidir.
Dağıtım bağlaşımı: Tek modüldeki kırık test tüm sürümü durdurur. Bu, mimari değil süreç problemidir; modül bazlı test paralelleştirmesi ve feature flag ile çözülür.
Derleme ve test süresi: Kod tabanı büyüdükçe pipeline uzar. Gradle/Maven çoklu modül yapısı ve yalnızca etkilenen modülleri test etmek bunu yönetilebilir tutar.
Teknoloji kilidi: Tüm modüller aynı Java sürümü ve framework'te ilerler. Kurumsal projelerde bu genellikle avantajdır.
Erozyon: Sınır zorlanmazsa üç yılda "big ball of mud" olur. Bu, aşağıda ele alacağımız araçlarla önlenir.
Dikkat: bu maddelerin çoğu modülerleştirme ve CI disipliniyle çözülür; ağ sınırı gerektirmez.
Karar için yedi soru
Mimari toplantılarında şu yedi soruyu sırayla soruyoruz. İlk beşe "hayır" diyorsanız mikroservisle başlamanın gerekçesi yoktur.
# | Soru | "Evet" ise |
|---|---|---|
1 | Birbirinden bağımsız yayın yapması gereken birden fazla ekip var mı? | Servis sınırı ekip sınırına oturabilir |
2 | Bir bileşenin ayrı ölçeklenmesi ölçümle kanıtlandı mı? | Önce worker/profil ayrımı dene; yetmezse servis |
3 | Bir bileşen farklı çalışma zamanı gerektiriyor mu (ör. Python ML modeli)? | O bileşen servis olur, gerisi değil |
4 | Düzenleyici izolasyon şart mı (PCI kapsamı, KVKK veri ayrımı)? | İlgili alan ayrı servis ve veritabanı |
5 | Operasyon olgunluğu var mı: CI/CD, tracing, merkezi log, nöbet? | Yoksa önce bunu kur |
6 | Alan sınırları (bounded context) net mi? | Değilse mikroservis yanlış sınırı dondurur |
7 | Modüller arası güçlü tutarlılık gerekiyor mu? | Gerekiyorsa aynı transaction sınırında kalsın |
Altıncı soru en çok atlanan sorudur. Yeni bir alanda sınırlar ilk altı ayda değişir; monolitte sınırı taşımak paket taşımaktır, mikroserviste veri göçü ve API sözleşmesi değişikliğidir.
Karşılaştırma: modüler monolit ve mikroservis
Boyut | Modüler monolit | Mikroservis |
|---|---|---|
Dağıtım birimi | Tek | Servis başına |
Veri tutarlılığı | ACID transaction | Saga / eventual consistency |
Modüller arası çağrı | Metot çağrısı, nanosaniye | HTTP/gRPC/mesaj, milisaniye + arıza modu |
Ölçekleme | Yatay kopya + worker profilleri | Servis başına bağımsız |
Ekip ölçeği | 1–3 ekip | Çok ekip, servis başına sahiplik |
Operasyon maliyeti | Düşük | Yüksek; platform ekibi gerektirir |
Hata ayıklama | Tek stack trace | Dağıtık tracing zorunlu |
İlk 6 ay geliştirme hızı | Yüksek | Düşük (altyapı kurulumu) |
Teknoloji çeşitliliği | Sınırlı | Serbest |
Yanlış sınır maliyeti | Düşük (paket taşı) | Yüksek (veri göçü + sözleşme) |
Varsayılan cevabımız: modüler monolit
Çoğu kurumsal projede önerimiz, mikroservise geçiş yolunu açık tutan bir modüler monolit. Spring Boot üzerinde bunu üç ilkeyle kuruyoruz.
1. Modül = paket, sınır = derleme zamanı
Her iş yeteneği (capability) uygulama paketinin altında bir modüldür. Dışa açık olan yalnızca modül kökündeki sınıflardır; alt paketler (internal gibi) diğer modüllere kapalıdır.
com.acme.erp
├── ErpApplication.java
├── siparis
│ ├── SiparisService.java // dışa açık API
│ ├── SiparisOlusturuldu.java // yayınlanan olay (record)
│ └── internal
│ ├── SiparisRepository.java
│ └── SiparisEntity.java
├── stok
├── fatura
└── musteriSınırı yorum satırıyla değil, testle koruyoruz. Spring Modulith bunu doğrudan sağlar:
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-test</artifactId>
<scope>test</scope>
</dependency>// siparis/package-info.java
@org.springframework.modulith.ApplicationModule(
displayName = "Sipariş",
allowedDependencies = { "musteri" } // stok ve fatura'ya doğrudan bağımlılık yasak
)
package com.acme.erp.siparis;class ModulYapisiTest {
@Test
void modulSinirlariIhlalEdilmiyor() {
ApplicationModules modules = ApplicationModules.of(ErpApplication.class);
modules.verify(); // stok -> siparis.internal erişimi varsa build kırılır
}
}verify() üç şeyi denetler: döngüsel bağımlılık, iç paketlere erişim ve allowedDependencies dışına çıkan bağımlılık. Spring Modulith kullanmak istemiyorsanız aynı kuralları ArchUnit ile yazabilirsiniz; önemli olan kuralın pipeline'da koşması.
2. Modüller arası iletişim: API çağrısı ya da olay, ikisi de sözleşme
Senkron ihtiyaçta modülün açık servisini çağırırız. İş akışının devamı başka bir modülün sorumluluğuysa olay yayınlarız; sipariş modülü stok modülünün varlığından habersizdir.
@Service
@Transactional
public class SiparisService {
private final SiparisRepository repository;
private final ApplicationEventPublisher events;
public SiparisNo olustur(SiparisTalebi talep) {
SiparisEntity siparis = repository.save(SiparisEntity.from(talep));
events.publishEvent(new SiparisOlusturuldu(siparis.getNo(), siparis.kalemler()));
return siparis.getNo();
}
}// stok modülü — siparis'e bağımlı değil, olayı dinler
@Component
class StokRezervasyonu {
@ApplicationModuleListener
void on(SiparisOlusturuldu event) {
rezerveEt(event.kalemler());
}
}@ApplicationModuleListener üç anotasyonun birleşimidir: @TransactionalEventListener (olay, sipariş transaction'ı commit olduktan sonra işlenir), @Async (@EnableAsync açık olmalı) ve @Transactional(propagation = REQUIRES_NEW). Yani stok rezervasyonu kendi transaction'ında koşar; siparişi geri almaz, kendi hatasını kendi yönetir.
Buradaki incelik event publication registry: spring-modulith-starter-jpa eklendiğinde yayınlanan her olay ve hangi dinleyicinin tamamladığı veritabanına yazılır. Dinleyici hata verirse kayıt "tamamlanmadı" olarak kalır ve yeniden işlenebilir. Bu, Kafka kurmadan elde edilen bir outbox garantisidir — ve ileride bir modülü servise çıkardığınızda olay @Externalized ile Kafka'ya yönlendirilir; yayınlayan servis kodu değişmez.
3. Veri sahipliği modül düzeyinde
Her modül kendi tablolarının sahibidir. Kural basit: modüller arası JOIN yok, başka modülün entity'sine JPA ilişkisi yok. Sipariş, müşteri bilgisine ihtiyaç duyuyorsa MusteriService.bul(id) çağırır ya da olayla aldığı veriyi kendi tablosuna kopyalar. Şema düzeyinde ayrım (siparis.*, stok.*) tercih edilir; böylece bir modül servise çıkarken tabloları "hangi tablo kimin" tartışması olmadan taşınır.
Bu üç ilke sağlandığında elinizde tek dağıtım birimi ama mikroservise hazır bir sistem vardır.
Ne zaman servis çıkarmalı: somut sinyaller
Modüler monolit, mikroservise karşı bir duruş değil, mikroservisi hak ettiği anda çıkarabilmek için bir hazırlıktır. Servis çıkarmayı şu sinyallerde gündeme alıyoruz:
Ölçüm destekli asimetrik yük: Bir modül CPU/bellek tüketiminin büyük kısmını tek başına alıyor ve worker profiline ayırmak yetmiyor (yoğun PDF üretimi, görüntü işleme gibi).
Farklı yayın temposu: Bir modül günde birkaç kez değişirken diğerleri ayda bir değişiyor ve ayrı bir ekibe geçti.
Farklı çalışma zamanı: Modelin Python'da koşması gerekiyor.
Uyum izolasyonu: Ödeme akışının PCI kapsamını daraltmak için ayrı ortama alınması gerekiyor.
Pipeline süresi: Modül bazlı test paralelleştirmesine rağmen build süresi ekibin akışını bozuyor.
Çıkarma yolu, sınırlar zaten korunduğu için kısadır: modülün açık API'sine bir REST/gRPC adaptörü yazılır, olaylar mesaj kuyruğuna yönlendirilir, tablolar taşınır, çağıranlar kademeli olarak yeni uca çevrilir (strangler fig). Sınır korunmamış bir monolitte aynı iş aylar sürer; korunmuş bir modülde haftalar.
Sık yapılan beş hata
Dağıtık monolit: Servisler ayrı ama veritabanı ortak, çağrılar senkron zincir. Mikroservisin tüm maliyetini ödeyip hiçbir getirisini almazsınız.
Entity başına servis: "User service, Product service, Order service" bölünmesi veri modeline göredir, iş yeteneğine göre değil; her iş akışı üç servise dokunur.
Dört kişilik ekiple mikroservis: Altyapı işi ürün işini geçer.
Idempotency'yi sonraya bırakmak: Retry ile çift sipariş, çift fatura üretilir. Bu, gerek monolitte gerek mikroserviste ilk günden tasarlanır.
Korelasyon kimliği olmadan yayına çıkmak: İlk üretim hatasında hangi isteğin nerede kırıldığı bulunamaz.
Sık sorulan sorular
Spring Boot ile mikroservis geliştirmek zor mu?
Framework tarafı değil; Spring Boot, Spring Cloud ve Resilience4j gerekli yapı taşlarını sağlar. Zorluk operasyondadır: dağıtım hattı, tracing, konfigürasyon ve nöbet. Bu altyapı yoksa mikroservisin kendisi zorlaşır.
Modüler monolitten mikroservise geçiş ne kadar sürer?
Modül sınırları testle korunmuşsa tek modülün çıkarılması tipik olarak birkaç haftalık iştir; büyük kalem veri göçü ve çağıranların yeni uca çevrilmesidir. Sınır korunmamışsa süre öngörülemez.
Mikroservis daha hızlı çalışır mı?
Hayır. Süreç içi çağrı ağ çağrısından yüzlerce kat hızlıdır. Mikroservisin kazanımı hız değil, bağımsız ölçekleme ve bağımsız dağıtımdır.
Hangi projelerde doğrudan mikroservisle başlanır?
Alan sınırlarının net olduğu, birden fazla özerk ekibin çalışacağı ve platform/altyapı ekibinin hazır olduğu projelerde. Bu üç koşul birlikte sağlanmıyorsa modüler monolit daha düşük riskli başlangıçtır.
Sonuç
Monolit–mikroservis kararı bir prestij meselesi değil, ekip yapısı, tutarlılık gereksinimi ve operasyon kapasitesinin fonksiyonudur. Çoğu kurumsal Java projesi için en düşük riskli yol, sınırları derleme zamanında zorlanan bir modüler monolitle başlamak ve yalnızca ölçümle kanıtlanmış ihtiyaçta servis çıkarmaktır.
Mevcut sisteminizin modül sınırlarını değerlendirmek ya da yeni bir projenin hedef mimarisini kurgulamak için yazılım tasarımı ve mimari hizmetimize göz atabilir, kurumsal uygulama geliştirme sürecimizi inceleyebilirsiniz. Keşif görüşmesi ve ilk tahmin ücretsizdir.