Modüler Monolith: Mikroservis Karmasasına Girmeden Önce Temiz Bir Mimari Kurmak
Yazılım dünyasında "Monolith" (Tek Parça) kötü, "Mikroservis" iyidir algısı o kadar yaygındır ki, ekipler 10 kişilik kullanıcısı olan projeler için bile Kubernetes ve Service Mesh kurmaya kalkışır. Oysa gerçek dünyada başarısız olan çoğu mikroservis projesi, aslında "Big Ball of Mud" (Karmakarışık Topak) olan monoliti, servislere bölerek sadece dağıtık sistem karmaşasını eklemiştir.
İşte bu noktada Modüler Monolith (Modüler Tekparça), en iyi iki dünyayı birleştirir: Tek bir deploy ünitesi (basitlik, kolay CI/CD) ve Sıkı modüler sınırlar (Domain Driven Design - Bounded Context). Bu yaklaşım, mikroservislere geçiş için de mükemmel bir basamaktır.
1. Modüler Monolith Nedir ve Ne Değildir?
-
Değildir: Katmanlı (N-Tier) mimari (UI > Business > Data). Buradaki modüller birbirine sıkı sıkıya bağlıdır (spaghetti).
-
Dır: Her biri kendi veritabanı şemasına (schema) veya tablo kümesine sahip, yalnızca belirli bir iş alanından (Bounded Context) sorumlu, birbirleriyle net sözleşmeler (contracts) üzerinden konuşan bağımsız modüller. Tüm modüller tek bir uygulama içinde çalışır ve tek bir çalıştırılabilir dosya (
.exe/.dll) olarak dağıtılır.
Mimarisi:
-
Modül A (Sipariş Yönetimi)→ Sadece Sipariş tablolarına erişir. -
Modül B (Stok Yönetimi)→ Sadece Stok tablolarına erişir. -
Modül A,Modül B'ye asla doğrudan veritabanı üzerinden erişmez;Modül B'nin sunduğu C# arayüzü (interface) veya In-Memory Event üzerinden haberleşir.
2. Neden Modüler Monolith? (Mikroservise Karşı Avantajları)
| Özellik | Mikroservis | Modüler Monolith |
|---|---|---|
| Ağ Gecikmesi (Network Latency) | Her çağrı HTTP/gRCP ile gider, ~100ms ek süre | In-memory metot çağrısı, ~ns seviyesinde (sıfır maliyet) |
| Dağıtık İşlem (Distributed Transaction) | Eventual Consistency + Saga pattern (çok zor) | ACID (Tek veritabanı olduğu için TransactionScope ile kolay) |
| CI/CD / Debug | 10 servis = 10 ayrı build/deploy, loglar farklı yerlerde | Tek build, tek deploy, tek log akışı |
| DevOps Maliyeti | Kubernetes, Service Discovery, API Gateway gerekir | Basit bir web sunucusu veya konsol yeterlidir |
| Ekip Organizasyonu | Her servis için ayrı ekip gerekir | Takım içi modül sorumlulukları verilebilir |
Peki neden herkes mikroservis ister? Çünkü modüler monolitin en büyük bedeli, disiplin gerektirmesidir. Modüller arası sınırları korumak, geliştiricilerin "kolaycılık" yapıp doğrudan diğer modülün tablosuna sorgu yazmasını engellemek zordur.
3. Modüler Monolit Nasıl İnşa Edilir? (Teknik Taktikler)
Taktik 1: Proje Yapısı (Solution Folders)
text
/ src
/ Modules
/ Orders (Modül)
/ Contracts (DI interfaceleri, DTO'lar)
/ Core (Domain Entity'ler, Repository interfaceleri)
/ Infrastructure (DbSet, Repository implementasyonları)
/ Inventory (Modül)
/ Contracts
/ Core
/ Infrastructure
/ Host (WebAPI veya Console) - Startup ve modül kayıtları
Taktik 2: Modül Kaydı (Discovery)
Her modül, kendi DI (Dependency Injection) kayıtlarını yapan bir IServiceCollection extension metodu sunar.
csharp
// Orders modülü kaydı
public static IServiceCollection AddOrdersModule(this IServiceCollection services)
{
services.AddDbContext<OrdersDbContext>();
services.AddScoped<IOrderRepository, OrderRepository>();
services.AddScoped<IOrderService, OrderService>();
return services;
}
// Program.cs'de tek bir yerden tüm modülleri yükle
builder.Services.AddOrdersModule();
builder.Services.AddInventoryModule();
Taktik 3: Veritabanı İzolasyonu (Schema veya Tablo Öneki)
Tek bir veritabanı kullanıyorsanız, her modüle farklı Schema (ör. orders.Orders, inventory.Products) verin. EF Core'da:
csharp
modelBuilder.HasDefaultSchema("orders");
Bu sayede farklı modüllerin tabloları fiziksel olarak ayrılır ve yanlışlıkla diğer modülün tablosuna erişmek imkânsız hale gelir.
Taktik 4: Modüller Arası İletişim (Sıkı Sözleşmeler)
Modül A, Modül B'nin verisine ihtiyaç duyuyorsa, Modül B'nin Contracts katmanındaki bir arayüzü (interface) çağırır.
csharp
// Inventory modülünün Contracts'ı
public interface IInventoryService
{
Task<bool> CheckStockAsync(int productId, int quantity);
}
// Orders modülü, InventoryService'in somut halini DI'dan alır.
public class OrderService
{
private readonly IInventoryService _inventory;
public OrderService(IInventoryService inventory) => _inventory = inventory;
}
⚠️ Kritik Kural: Modül A, Modül B'nin Infrastructure veya Core katmanına asla referans veremez. Sadece Contracts'a güvenir.
4. Modüller Arası Event (Olay) ile Gevşek Bağlantı (MediatR Kullanımı)
Doğrudan servis çağrısı (senkron) hala bağımlılık yaratır. İdeal olan In-Memory Event Bus kullanmaktır. (MediatR veya direkt .NET EventHandler).
-
Sipariş oluşturuldu →
OrderCreatedEventfırlatılır. -
Stok modülü bu event'i yakalar ve stoktan düşer.
-
Bu sayede iki modül birbirini tanımaz, sadece event'i tanır.
csharp
// Orders modülünde
await mediator.Publish(new OrderCreatedEvent(orderId, productId, quantity));
// Inventory modülünde (Handler)
public class OrderCreatedHandler : INotificationHandler<OrderCreatedEvent>
{
public Task Handle(OrderCreatedEvent notification, CancellationToken ct)
{
// Stok düşme işlemi
}
}
5. Modüler Monolit'ten Mikroservis'e Geçiş (Köprü Stratejisi)
Modüler monolitin en büyük süper gücü budur. Eğer bir modül (ör. Sipariş) çok büyür ve ekip ayrılması gerekiyorsa:
-
Contractskatmanını ayrı bir NuGet paketi veya paylaşılan proje haline getirin. -
O modülün veritabanını fiziksel olarak yeni bir sunucuya taşıyın.
-
Modül içindeki in-memory çağrıları (metot çağrıları)
HttpClientveyagRPCçağrılarına dönüştürün (çok az değişiklikle). -
Yeni servisi ayrı bir endpoint'te deploy edin.
Hiçbir kod yeniden yazmak zorunda kalmazsınız çünkü sınırlar (bounded contexts) zaten çizilmiştir.
6. Sık Yapılan Hatalar ve Çözümleri
| Hata | Çözüm |
|---|---|
| "Paylaşımlı Veritabanı" (Shared DB) modülleri birbirine bağlar | Her modüle ayrı schema verin; modül A, modül B'nin tablosuna JOIN atamaz hale gelsin. |
| Modüller arası doğrudan Entity kullanımı | Asla Order entity'sini Inventory modülüne göndermeyin. Bunun yerine OrderDto veya sadece OrderId gönderin. |
| Modüller arası döngüsel bağımlılık (A->B, B->A) | Event (MediatR) kullanarak döngüyü kırın. A, B'yi çağırmaz; A event fırlatır, B yakalar. |
| "Her şeyi paylaşalım" (Shared Kernel) tuzağı | Çok fazla ortak (shared) kod tanımlamayın. Ortak kod, modüller arası bağımlılığı artırır. Mümkünse her modül kendi helper'ını yazsın. |
Sonuç:
Modüler Monolith, mikroservis çözümlerinin karmaşıklığına dalmadan önce uygulanması gereken olgun bir başlangıç noktasıdır. Ürününüz büyüdüğünde ve gerçekten ölçeklendirme (scaling) ihtiyacı doğduğunda, bu yapı size mikroservislere sıfırdan yazmak yerine evrilerek (strangler pattern) geçme imkânı sunar.
Altın Kural: Monolitik dağıtım (deployment) yapabilirsiniz, ancak asla monolitik tasarım (design) yapmayın. Kodunuzu iş sınırlarına (bounded contexts) göre ayırın, veritabanı erişimini kısıtlayın ve modüller arasında sıkı sözleşmeler (contracts) uygulayın. Gerisi (ölçeklendirme, ekip büyümesi) bu temel üzerine inşa edilir.