← Blog

.NET ile Modüler Monolit: Mikroservise Geçmeden Önce Okuyun

Mikroservisler ciddi bir operasyonel maliyet getirir. Çoğu kurumsal uygulama için daha doğru başlangıç: sınırları net, tek parça dağıtılan modüler bir monolit. .NET ve EF Core ile nasıl kurulacağını örneklerle anlattık.

Mikroservis mimarisi son yıllarda neredeyse her yeni projenin varsayılan cevabı haline geldi. Oysa mikroservisler ciddi bir bedelle gelir: ağ üzerinden haberleşme, dağıtık işlemler, servis başına dağıtım, izleme ve sürüm yönetimi. Bu bedel, ekip ve ürün belirli bir ölçeğe ulaşmadan ödendiğinde, proje çözdüğünden çok daha fazla sorun üretir.

Kurumsal uygulamaların büyük kısmı için daha doğru başlangıç noktası modüler monolittir: tek parça olarak dağıtılan, ama içinde sınırları net çizilmiş modüllerden oluşan bir uygulama.

Modüler monolit nedir, ne değildir?

Klasik monolitte her katman her şeye erişebilir; zamanla stok kodu fatura tablosunu doğrudan günceller, satış ekranı üretim servisinin iç sınıflarını çağırır ve sistem "büyük bir çamur topu"na dönüşür.

Modüler monolitte ise:

  • Her modül (Stok, Finans, Satış, Üretim) kendi iş kurallarına ve verisine sahiptir.
  • Modüller birbirleriyle yalnızca açıkça tanımlanmış sözleşmeler üzerinden konuşur.
  • Uygulama tek bir süreç olarak çalışır ve tek bir veritabanı sunucusunu kullanabilir.

Kısacası mikroservislerin mantıksal ayrımını, monolitin operasyonel sadeliğiyle birleştirir.

Proje yapısı

.NET tarafında her modülü ayrı projelere bölmek, sınırları derleyici seviyesinde korumanın en kolay yoludur:

src/
  Erp.Host/                 → ASP.NET Core giriş noktası, modülleri kaydeder
  Modules/
    Stok/
      Stok.Contracts/       → dışarıya açık arayüzler ve olaylar
      Stok.Core/            → iş kuralları, entity'ler (internal)
      Stok.Infrastructure/  → EF Core, sorgular
    Finans/
      Finans.Contracts/
      Finans.Core/
      Finans.Infrastructure/
  Shared/
    Shared.Kernel/          → ortak temel tipler (Result, IDomainEvent…)

Kural basittir: bir modül, başka bir modülün yalnızca Contracts projesine referans verebilir. Core ve Infrastructure içindeki tipler internal tutulur.

Modüller arası iletişim: senkron sözleşmeler

Bir modülün diğerinden anlık bilgiye ihtiyacı olduğunda, sözleşme projesinde tanımlanan bir arayüz kullanılır:

// Stok.Contracts — Stok modülünün dışarıya açtığı tek kapı
public interface IStokModulu
{
    Task<decimal> BakiyeAsync(int stokId, int depoId, CancellationToken ct);
    Task<bool> RezerveEtAsync(int stokId, int depoId, decimal miktar, CancellationToken ct);
}

Satış modülü bu arayüzü bilir, ama stok bakiyesinin hangi tablolardan nasıl hesaplandığını bilmez. Stok modülünün iç yapısı değiştiğinde satış modülü etkilenmez.

Modüller arası iletişim: olaylar

"Fatura onaylandığında stok düşsün, cari hesap güncellensin, muhasebe fişi oluşsun" gibi akışlar için doğrudan çağrı yerine olay (event) kullanmak bağımlılığı azaltır. Finans modülü olayı yayınlar; ilgilenen modüller kendi işini yapar:

// Finans.Contracts
public sealed record FaturaOnaylandi(int FaturaId, IReadOnlyList<FaturaSatiri> Satirlar) : IDomainEvent;

// Stok.Core — Finans modülünü tanımadan sadece olaya tepki verir
internal sealed class FaturaOnaylandiStokDus(StokDbContext db) : IDomainEventHandler<FaturaOnaylandi>
{
    public async Task HandleAsync(FaturaOnaylandi e, CancellationToken ct)
    {
        foreach (var s in e.Satirlar)
            db.Hareketler.Add(StokHareket.Cikis(s.StokId, s.DepoId, s.Miktar, kaynak: $"FTR-{e.FaturaId}"));

        await db.SaveChangesAsync(ct);
    }
}

Veri sınırları: modül başına şema

Aynı veritabanını kullanmak, modüllerin birbirinin tablosuna dokunabileceği anlamına gelmemeli. SQL Server'da her modül için ayrı bir şema kullanmak ve her modüle ayrı bir DbContext vermek bu sınırı görünür kılar:

internal sealed class StokDbContext(DbContextOptions<StokDbContext> options) : DbContext(options)
{
    public DbSet<StokKarti> Kartlar => Set<StokKarti>();
    public DbSet<StokHareket> Hareketler => Set<StokHareket>();

    protected override void OnModelCreating(ModelBuilder b)
    {
        b.HasDefaultSchema("stok");   // stok.Kartlar, stok.Hareketler
        b.ApplyConfigurationsFromAssembly(typeof(StokDbContext).Assembly);
    }
}

Raporlama gibi modüller arası okuma gereken yerlerde ise ayrı, salt okunur görünümler (view) ya da raporlama sorguları kullanılabilir.

Sınırları korumak

Kurallar yazılı kalırsa zamanla delinir. Bu yüzden sınırları otomatik testlerle koruyoruz: "Satış modülü, Stok.Core'a referans veremez" gibi kurallar mimari testlerle her derlemede kontrol edilir. Kural ihlali, kod incelemesine bile gelmeden yakalanır.

Peki ne zaman mikroservis?

Modüler monolit kalıcı bir hapishane değil, sağlam bir başlangıçtır. Bir modül şu durumlardan birine geldiğinde ayrı bir servise taşınmayı hak eder:

  • Diğerlerinden çok farklı bir ölçeklenme ihtiyacı varsa (ör. yoğun entegrasyon trafiği)
  • Ayrı bir ekip tarafından, ayrı bir yayın takvimiyle geliştiriliyorsa
  • Farklı bir teknoloji veya altyapı gerektiriyorsa

Sınırlar baştan doğru çizildiği için bu taşıma, sözleşmenin arkasındaki uygulamanın bir ağ çağrısına dönüşmesinden ibarettir. Mimariyi yeniden yazmanız gerekmez.

Özet

KonuModüler monolit yaklaşımı
DağıtımTek uygulama, tek yayın
Modül sınırıAyrı projeler, internal iç tipler
İletişimSözleşme arayüzleri ve olaylar
VeriModül başına şema ve DbContext
KorumaMimari testler
GelecekGerekirse modül modül servise taşınabilir

DayanSoft'ta ERP ve kurumsal uygulamaları bu yaklaşımla geliştiriyoruz: bugün sade ve hızlı, yarın büyümeye hazır.

Benzer bir projeniz mi var?

İhtiyacınızı anlatın; size özel bir yol haritası ve tahmini bir plan çıkaralım.

Bize Ulaşın →