← Back to all posts

Monolith'ten Modüler Monolith'e Geçiş

Monolith'ten Modüler Monolith'e Geçiş

Monolith'ten Modüler Monolith'e Geçiş

Yanlış İkili — Monolith mi, Mikroservis mi?

Yazılım mimarisi tartışmalarında tablo genellikle şöyle çizilir:

Monolith = Kötü, Eski, Ölçeklenemez
Mikroservis = İyi, Modern, Sonsuz Ölçeklenebilir

Bu tablo yanlıştır. Ve bu yanlış tabloya inanan ekipler, olgunlaşmamış bir uygulamayı erken mikroservise taşıyarak kendileriyle karmaşıklık satın alırlar.

Dağıtık sistemlerin getirdiği problemler gerçektir: network gecikmesi, dağıtık transaction yönetimi, servisler arası sözleşme uyumu, deployment karmaşıklığı, izleme zorluğu. Bu maliyetleri karşılayacak ölçek ve ekip büyüklüğüne ulaşmadan bu yola girmek tehlikelidir.

Modüler monolith bu ikisi arasındaki mantıklı duraktır.

Tek bir deployable unit olarak çalışır — basitlik korunur. Ama içeride net sınırlarla ayrılmış modüller vardır — karmaşıklık yönetilir. İhtiyaç duyulursa modüller mikroservise dönüştürülebilir — esneklik hazır bekler.


Monolith Nerede Bozulur?

Her monolith başlangıçta masumdur. Sorunlar zamanla ve fark edilmeden birikerek gelir.

Spaghetti Bağımlılıkları

// OrderService'in bağımlılık listesi — gerçek bir projeden
public class OrderService
{
    private readonly IOrderRepository _orderRepo;
    private readonly ICustomerRepository _customerRepo;
    private readonly IProductRepository _productRepo;
    private readonly IInventoryRepository _inventoryRepo;
    private readonly IPaymentService _paymentService;
    private readonly IShipmentService _shipmentService;
    private readonly INotificationService _notificationService;
    private readonly IInvoiceService _invoiceService;
    private readonly IDiscountService _discountService;
    private readonly ICampaignService _campaignService;
    private readonly ILoyaltyService _loyaltyService;
    private readonly IReportService _reportService;
    private readonly IEmailService _emailService;
    private readonly ISmsService _smsService;
    private readonly IAuditService _auditService;
    // devam ediyor...
}

Bu sınıfı gördüğünüzde şunu bilirsiniz: her şey her şeye bağımlı. Bir yerde yapılan değişiklik nerede etki yaratacağını kimse bilmiyor.

God Class Sendromu

// OrderService.cs — 4000 satır
public class OrderService
{
    public Task CreateOrder(...) { }
    public Task CancelOrder(...) { }
    public Task CalculateDiscount(...) { }
    public Task ProcessPayment(...) { }
    public Task SendConfirmationEmail(...) { }
    public Task UpdateInventory(...) { }
    public Task GenerateInvoice(...) { }
    public Task CalculateLoyaltyPoints(...) { }
    public Task CreateShipment(...) { }
    public Task UpdateCustomerStats(...) { }
    // 50 metod daha...
}

Belirtiler — Refactor Zamanı Gelmiş

⚠️ Küçük bir değişiklik için büyük risk — her şey birbirine dokunuyor
⚠️ Test yazmak neredeyse imkânsız — bağımlılıklar çok derin
⚠️ Yeni geliştirici projeye hakim olamıyor — "nereden başlayacağım?"
⚠️ Build süresi uzadı — her değişiklik her şeyi yeniden derliyor
⚠️ Ekipler birbirini bekliyor — aynı dosyalara dokunuyorlar
⚠️ Deployment korkusu — "ne kopar bilmiyorum"

Modüler Monolith Nedir?

Modüler monolith, tek bir process olarak çalışan ama içeride net domain sınırlarıyla ayrılmış bir uygulamadır.

┌─────────────────────────────────────────────┐
│              Modüler Monolith                │
│                                             │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  │
│  │  Orders  │  │ Payments │  │Inventory │  │
│  │  Module  │  │  Module  │  │  Module  │  │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  │
│       │              │              │        │
│  ┌────▼──────────────▼──────────────▼─────┐ │
│  │         Shared Kernel / Core           │ │
│  └────────────────────────────────────────┘ │
│                                             │
│              Tek Veritabanı                 │
└─────────────────────────────────────────────┘
         ↕ Tek Deployment Unit

Temel Prensipler

1. Her modül kendi sınırına sahiptir
Modüller birbirinin internal sınıflarına doğrudan erişemez. Sadece public interface'ler üzerinden iletişim kurulur.

2. Her modül kendi verisine sahiptir
Tek veritabanı olsa bile her modülün kendi şema veya tablo prefix'i vardır. Başka modülün tablosuna direkt JOIN yapılmaz.

3. Modüller arası iletişim contract üzerinden olur
Event'ler, interface'ler veya servis sözleşmeleri — doğrudan sınıf referansı değil.


Proje Yapısı

Klasik Monolith Yapısı — Sorunlu

src/
├── Controllers/
│   ├── OrderController.cs
│   ├── PaymentController.cs
│   └── InventoryController.cs
├── Services/
│   ├── OrderService.cs
│   ├── PaymentService.cs
│   └── InventoryService.cs
├── Repositories/
│   ├── OrderRepository.cs
│   ├── PaymentRepository.cs
│   └── InventoryRepository.cs
└── Models/
    ├── Order.cs
    ├── Payment.cs
    └── Inventory.cs

Bu yapıda katmanlar vardır ama domain sınırları yoktur. OrderService her şeye erişebilir.

Modüler Monolith Yapısı — Hedef

src/
├── Modules/
│   ├── Orders/
│   │   ├── MyApp.Orders/              ← Modül projesi
│   │   │   ├── Domain/
│   │   │   │   ├── Order.cs
│   │   │   │   ├── OrderItem.cs
│   │   │   │   └── OrderStatus.cs
│   │   │   ├── Application/
│   │   │   │   ├── Commands/
│   │   │   │   ├── Queries/
│   │   │   │   └── IOrdersModule.cs   ← Public interface
│   │   │   ├── Infrastructure/
│   │   │   │   ├── OrdersDbContext.cs
│   │   │   │   └── Repositories/
│   │   │   └── OrdersModule.cs        ← Modül kaydı
│   │
│   ├── Payments/
│   │   └── MyApp.Payments/
│   │       ├── Domain/
│   │       ├── Application/
│   │       │   └── IPaymentsModule.cs
│   │       ├── Infrastructure/
│   │       └── PaymentsModule.cs
│   │
│   ├── Inventory/
│   │   └── MyApp.Inventory/
│   │       ├── Domain/
│   │       ├── Application/
│   │       │   └── IInventoryModule.cs
│   │       ├── Infrastructure/
│   │       └── InventoryModule.cs
│   │
│   └── Notifications/
│       └── MyApp.Notifications/
│
├── Shared/
│   └── MyApp.Shared/                  ← Ortak kernel
│       ├── Domain/
│       │   ├── BaseEntity.cs
│       │   └── IDomainEvent.cs
│       ├── Contracts/                 ← Modüller arası sözleşmeler
│       │   ├── Orders/
│       │   │   └── OrderCreatedEvent.cs
│       │   └── Payments/
│       │       └── PaymentCompletedEvent.cs
│       └── Infrastructure/
│           └── IEventBus.cs
│
└── MyApp.API/                         ← Tek giriş noktası
    ├── Program.cs
    └── appsettings.json

Modül Tanımı ve Kaydı

Her modül kendini sisteme kaydetmekten sorumludur. Dışarıdan ne sunduğunu public interface ile ilan eder.

Public Modül Interface'i

// MyApp.Orders/Application/IOrdersModule.cs
// Bu interface modülün dışarıya sunduğu tek yüzeydir
public interface IOrdersModule
{
    Task<Guid> CreateOrderAsync(CreateOrderRequest request);
    Task<OrderDto?> GetOrderAsync(Guid orderId);
    Task<bool> CancelOrderAsync(Guid orderId, string reason);
}

// Diğer modüllerin görebileceği DTO'lar
public record CreateOrderRequest(
    Guid CustomerId,
    List<OrderLineRequest> Lines,
    string DeliveryAddress);

public record OrderDto(
    Guid Id,
    string Status,
    decimal TotalAmount,
    DateTime CreatedAt);

Modül Implementation

// MyApp.Orders/Application/OrdersModule.cs
internal class OrdersModule : IOrdersModule
{
    private readonly IMediator _mediator;

    public OrdersModule(IMediator mediator)
    {
        _mediator = mediator;
    }

    public async Task<Guid> CreateOrderAsync(CreateOrderRequest request)
    {
        return await _mediator.Send(new CreateOrderCommand(
            request.CustomerId,
            request.Lines.Select(l => new OrderLineDto(l.ProductId, l.Quantity)).ToList(),
            request.DeliveryAddress));
    }

    public async Task<OrderDto?> GetOrderAsync(Guid orderId)
    {
        return await _mediator.Send(new GetOrderByIdQuery(orderId));
    }

    public async Task<bool> CancelOrderAsync(Guid orderId, string reason)
    {
        await _mediator.Send(new CancelOrderCommand(orderId, reason));
        return true;
    }
}

Modül DI Kaydı

// MyApp.Orders/OrdersModuleExtensions.cs
public static class OrdersModuleExtensions
{
    public static IServiceCollection AddOrdersModule(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        // DbContext — Orders modülünün kendi context'i
        services.AddDbContext<OrdersDbContext>(options =>
            options.UseSqlServer(
                configuration.GetConnectionString("DefaultConnection"),
                sql => sql.MigrationsAssembly("MyApp.Orders")));

        // MediatR handlers
        services.AddMediatR(cfg =>
            cfg.RegisterServicesFromAssembly(typeof(OrdersModuleExtensions).Assembly));

        // FluentValidation
        services.AddValidatorsFromAssembly(typeof(OrdersModuleExtensions).Assembly);

        // Repository'ler — internal, dışarıdan erişilemez
        services.AddScoped<IOrderRepository, OrderRepository>();

        // Public interface kaydı
        services.AddScoped<IOrdersModule, OrdersModule>();

        return services;
    }
}

Ana Kayıt — Program.cs

// MyApp.API/Program.cs
var builder = WebApplication.CreateBuilder(args);

// Her modül tek satırda eklenir
builder.Services
    .AddOrdersModule(builder.Configuration)
    .AddPaymentsModule(builder.Configuration)
    .AddInventoryModule(builder.Configuration)
    .AddNotificationsModule(builder.Configuration);

builder.Services.AddControllers();

var app = builder.Build();
app.MapControllers();
app.Run();

Modüller Arası İletişim

Modüller birbirinin internal sınıflarına erişemez. İletişim iki yolla olur:

1. Doğrudan Interface Çağrısı — Senkron

Bir modülün diğerinin public interface'ini çağırması. Basit ama dikkatli kullanılmalıdır — fazla bağımlılık modül sınırlarını eritir.

// Payments modülü, Orders modülünü kullanıyor
public class ProcessPaymentCommandHandler
    : IRequestHandler<ProcessPaymentCommand, PaymentResult>
{
    private readonly IOrdersModule _ordersModule;   // Interface üzerinden
    private readonly IPaymentRepository _paymentRepo;

    public ProcessPaymentCommandHandler(
        IOrdersModule ordersModule,
        IPaymentRepository paymentRepo)
    {
        _ordersModule = ordersModule;
        _paymentRepo = paymentRepo;
    }

    public async Task<PaymentResult> Handle(
        ProcessPaymentCommand command,
        CancellationToken ct)
    {
        // Sipariş bilgisini Orders modülünden al
        var order = await _ordersModule.GetOrderAsync(command.OrderId)
            ?? throw new NotFoundException("Sipariş bulunamadı.");

        // Ödeme işlemini gerçekleştir
        var payment = Payment.Create(order.Id, order.TotalAmount);
        await _paymentRepo.AddAsync(payment, ct);

        return new PaymentResult(payment.Id, payment.Status);
    }
}

2. Domain Event — Asenkron

Modüller arası iletişimin tercih edilen yoludur. Yayımlayan modül olayı bildirir, ilgilenen modüller dinler. Gevşek bağımlılık sağlar.

// Shared/Contracts/Orders/OrderCreatedEvent.cs
// Her iki modülün de görebileceği sözleşme
public record OrderCreatedEvent(
    Guid OrderId,
    Guid CustomerId,
    decimal TotalAmount,
    DateTime CreatedAt) : IDomainEvent;

// Orders modülü — event yayımlar
public class CreateOrderCommandHandler
    : IRequestHandler<CreateOrderCommand, Guid>
{
    private readonly OrdersDbContext _context;
    private readonly IEventBus _eventBus;

    public CreateOrderCommandHandler(
        OrdersDbContext context,
        IEventBus eventBus)
    {
        _context = context;
        _eventBus = eventBus;
    }

    public async Task<Guid> Handle(
        CreateOrderCommand command,
        CancellationToken ct)
    {
        var order = Order.Create(command.CustomerId, command.DeliveryAddress);

        foreach (var line in command.Lines)
            order.AddItem(line.ProductId, line.Quantity);

        _context.Orders.Add(order);
        await _context.SaveChangesAsync(ct);

        // Event yayımla — başka kim ilgilenirse ilgilensin
        await _eventBus.PublishAsync(new OrderCreatedEvent(
            order.Id,
            order.CustomerId,
            order.TotalAmount,
            order.CreatedAt), ct);

        return order.Id;
    }
}

// Inventory modülü — event'i dinler
public class OrderCreatedEventHandler
    : INotificationHandler<OrderCreatedEvent>
{
    private readonly IInventoryRepository _inventoryRepo;

    public OrderCreatedEventHandler(IInventoryRepository inventoryRepo)
    {
        _inventoryRepo = inventoryRepo;
    }

    public async Task Handle(
        OrderCreatedEvent notification,
        CancellationToken ct)
    {
        // Stok rezervasyonu yap
        await _inventoryRepo.ReserveStockForOrderAsync(
            notification.OrderId, ct);
    }
}

// Notifications modülü — aynı event'i dinler
public class OrderCreatedNotificationHandler
    : INotificationHandler<OrderCreatedEvent>
{
    private readonly IEmailService _emailService;

    public OrderCreatedNotificationHandler(IEmailService emailService)
    {
        _emailService = emailService;
    }

    public async Task Handle(
        OrderCreatedEvent notification,
        CancellationToken ct)
    {
        await _emailService.SendOrderConfirmationAsync(
            notification.CustomerId,
            notification.OrderId, ct);
    }
}

In-Process Event Bus

Aynı process içinde çalışan basit bir event bus implementasyonu:

// Shared/Infrastructure/InProcessEventBus.cs
public class InProcessEventBus : IEventBus
{
    private readonly IMediator _mediator;
    private readonly ILogger<InProcessEventBus> _logger;

    public InProcessEventBus(IMediator mediator, ILogger<InProcessEventBus> logger)
    {
        _mediator = mediator;
        _logger = logger;
    }

    public async Task PublishAsync<TEvent>(TEvent @event, CancellationToken ct = default)
        where TEvent : IDomainEvent
    {
        _logger.LogInformation(
            "Event yayımlanıyor: {EventType}", typeof(TEvent).Name);

        await _mediator.Publish(@event, ct);
    }
}

// Program.cs kaydı
builder.Services.AddScoped<IEventBus, InProcessEventBus>();

İleride mikroservise geçilirse InProcessEventBus yerine RabbitMqEventBus veya ServiceBusEventBus enjekte edilir. Uygulama kodu değişmez.


Veritabanı Stratejisi

Modüler monolith'te tek bir veritabanı kullanılır ama her modülün verileri mantıksal olarak ayrılır.

Şema Bazlı Ayrım — Önerilen

-- Her modülün kendi şeması
CREATE SCHEMA orders;
CREATE SCHEMA payments;
CREATE SCHEMA inventory;
CREATE SCHEMA notifications;

-- Orders modülünün tabloları
CREATE TABLE orders.Orders (...);
CREATE TABLE orders.OrderItems (...);

-- Payments modülünün tabloları
CREATE TABLE payments.Payments (...);
CREATE TABLE payments.PaymentMethods (...);

-- Inventory modülünün tabloları
CREATE TABLE inventory.Products (...);
CREATE TABLE inventory.StockMovements (...);

Her Modülün Kendi DbContext'i

// MyApp.Orders/Infrastructure/OrdersDbContext.cs
public class OrdersDbContext : DbContext
{
    public DbSet<Order> Orders => Set<Order>();
    public DbSet<OrderItem> OrderItems => Set<OrderItem>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Sadece orders şeması
        modelBuilder.HasDefaultSchema("orders");
        modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly);
    }
}

// MyApp.Payments/Infrastructure/PaymentsDbContext.cs
public class PaymentsDbContext : DbContext
{
    public DbSet<Payment> Payments => Set<Payment>();
    public DbSet<PaymentMethod> PaymentMethods => Set<PaymentMethod>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.HasDefaultSchema("payments");
        modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly);
    }
}

Cross-Module JOIN Yasağı

-- ❌ Yasak — farklı modüllerin tablolarını direkt JOIN yapma
SELECT o.OrderId, p.Amount
FROM orders.Orders o
INNER JOIN payments.Payments p ON p.OrderId = o.OrderId;

-- ✅ Doğru — her modül kendi verisini döndürür,
-- API katmanı birleştirir veya event'lerle veri kopyalanır

Geçiş Stratejisi — Adım Adım

Büyük bir monolith'i tek seferinde dönüştürmeye çalışmayın. Bu "big bang refactoring"dir ve başarı oranı düşüktür.

Adım 1 — Modül Sınırlarını Belirleyin

Koda dokunmadan önce domain sınırlarını çizin:

Domain Analizi:

Orders Modülü:
  - Sipariş oluşturma, iptal, güncelleme
  - Sipariş kalemleri
  - Teslimat adresi

Payments Modülü:
  - Ödeme işleme
  - İade
  - Ödeme yöntemleri

Inventory Modülü:
  - Stok takibi
  - Stok hareketi
  - Rezervasyon

Notifications Modülü:
  - Email, SMS gönderimi
  - Bildirim tercihleri
  - Şablonlar

Shared Kernel:
  - BaseEntity, IDomainEvent
  - Modüller arası event sözleşmeleri
  - Ortak value object'ler (Money, Address)

Sınırları test etme soruları:

  • Bu modül silinse hangi modüller etkilenir?
  • Bu iki özellik her zaman birlikte mi değişir?
  • Farklı ekipler bu alanları bağımsız geliştirebilir mi?

Adım 2 — Shared Kernel Oluşturun

// MyApp.Shared/Domain/BaseEntity.cs
public abstract class BaseEntity
{
    public Guid Id { get; protected set; }
    public DateTime CreatedAt { get; protected set; }

    private readonly List<IDomainEvent> _events = new();
    public IReadOnlyCollection<IDomainEvent> DomainEvents => _events.AsReadOnly();

    protected void AddDomainEvent(IDomainEvent @event) => _events.Add(@event);
    public void ClearDomainEvents() => _events.Clear();
}

// MyApp.Shared/Domain/IDomainEvent.cs
public interface IDomainEvent : INotification { }

// MyApp.Shared/Contracts/Orders/OrderCreatedEvent.cs
public record OrderCreatedEvent(
    Guid OrderId,
    Guid CustomerId,
    decimal TotalAmount,
    DateTime CreatedAt) : IDomainEvent;

Adım 3 — Bir Modülü Pilot Olarak Seçin

En az bağımlılığı olan, en iyi anlaşılan domain modülüyle başlayın:

✅ İyi pilot modül:
   - Notifications — çoğu şeyden event alır, kimseye bağımlı değildir
   - Inventory — bağımlılıkları az, sınırları net

❌ Zor pilot modül:
   - Orders — çok fazla şeye bağlı
   - Payments — kritik iş mantığı, risk yüksek

Adım 4 — Strangler Fig Pattern

Mevcut kodu silmeden yeni modülü yanına büyütün. Hazır olduğunda trafiği yeni yapıya yönlendirin.

// Geçiş süreci — eski ve yeni birlikte çalışıyor
public class OrderController : ControllerBase
{
    private readonly IOrdersModule _newModule;        // Yeni modül
    private readonly OldOrderService _oldService;    // Eski servis
    private readonly IFeatureFlag _featureFlag;

    [HttpPost]
    public async Task<IActionResult> Create(CreateOrderRequest request)
    {
        // Feature flag ile kademeli geçiş
        if (_featureFlag.IsEnabled("UseNewOrdersModule"))
        {
            var orderId = await _newModule.CreateOrderAsync(request);
            return Ok(new { OrderId = orderId });
        }

        // Eski yol
        var result = await _oldService.CreateOrderAsync(request);
        return Ok(result);
    }
}

Adım 5 — Controller'ları Modüle Taşıyın

// Her modül kendi endpoint'lerini tanımlayabilir
// MyApp.Orders/Presentation/OrdersEndpoints.cs
public static class OrdersEndpoints
{
    public static IEndpointRouteBuilder MapOrdersEndpoints(
        this IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/orders")
            .WithTags("Orders")
            .RequireAuthorization();

        group.MapPost("/", CreateOrder);
        group.MapGet("/{id:guid}", GetOrder);
        group.MapDelete("/{id:guid}", CancelOrder);

        return app;
    }

    private static async Task<IResult> CreateOrder(
        CreateOrderRequest request,
        IOrdersModule module)
    {
        var orderId = await module.CreateOrderAsync(request);
        return Results.Created($"/api/orders/{orderId}", new { OrderId = orderId });
    }

    private static async Task<IResult> GetOrder(
        Guid id,
        IOrdersModule module)
    {
        var order = await module.GetOrderAsync(id);
        return order is null ? Results.NotFound() : Results.Ok(order);
    }

    private static async Task<IResult> CancelOrder(
        Guid id,
        [FromBody] CancelOrderRequest request,
        IOrdersModule module)
    {
        await module.CancelOrderAsync(id, request.Reason);
        return Results.NoContent();
    }
}

// Program.cs
app.MapOrdersEndpoints();
app.MapPaymentsEndpoints();
app.MapInventoryEndpoints();

Modül Sınırı İhlallerini Tespit Etmek

Zamanla sınırlar erimaya başlar. Bunu önlemek için statik analiz araçları kullanın.

// NetArchTest ile mimari kuralları test edin
dotnet add package NetArchTest.Rules
// Tests/ArchitectureTests.cs
public class ModuleBoundaryTests
{
    [Fact]
    public void Orders_Module_Should_Not_Depend_On_Payments_Internals()
    {
        var result = Types
            .InAssembly(typeof(OrdersModule).Assembly)
            .Should()
            .NotHaveDependencyOn("MyApp.Payments.Infrastructure")
            .GetResult();

        result.IsSuccessful.Should().BeTrue(
            "Orders modülü Payments modülünün internal sınıflarına erişemez.");
    }

    [Fact]
    public void Payments_Module_Should_Not_Depend_On_Orders_Infrastructure()
    {
        var result = Types
            .InAssembly(typeof(PaymentsModule).Assembly)
            .Should()
            .NotHaveDependencyOn("MyApp.Orders.Infrastructure")
            .GetResult();

        result.IsSuccessful.Should().BeTrue();
    }

    [Fact]
    public void All_Modules_Should_Only_Communicate_Through_Shared_Contracts()
    {
        // Modüller sadece MyApp.Shared üzerinden iletişim kurabilir
        var ordersAssembly = typeof(OrdersModule).Assembly;

        var result = Types
            .InAssembly(ordersAssembly)
            .That()
            .HaveDependencyOn("MyApp.Payments")
            .Should()
            .OnlyHaveDependencyOn("MyApp.Shared")
            .GetResult();

        result.IsSuccessful.Should().BeTrue();
    }
}

Mikroservise Geçiş Hazırlığı

Modüler monolith kuruluysa mikroservise geçiş teknik değil, operasyonel bir karardır.

Modüler Monolith → Mikroservis geçişi için:

1. Her modülün kendi DbContext'i var       ✅ Hazır
2. Modüller interface üzerinden konuşuyor  ✅ Hazır
3. Event-based iletişim kurulmuş           ✅ Hazır
4. InProcessEventBus → RabbitMqEventBus   → Sadece bu değişir
5. Ayrı deployment                         → CI/CD pipeline güncellenir
6. Servis discovery                        → Yeni altyapı gerekir
// Geçiş: sadece IEventBus implementation'ı değişir
// Uygulama kodu dokunulmaz

// Önce — aynı process
builder.Services.AddScoped<IEventBus, InProcessEventBus>();

// Sonra — RabbitMQ ile dağıtık
builder.Services.AddScoped<IEventBus, RabbitMqEventBus>();

Karşılaştırma Tablosu

Kriter Monolith Modüler Monolith Mikroservis
Başlangıç karmaşıklığı ✅ Düşük ✅ Orta ❌ Yüksek
Deployment ✅ Basit ✅ Basit ❌ Karmaşık
Ölçeklendirme ❌ Tüm uygulama ❌ Tüm uygulama ✅ Servis bazlı
Ekip bağımsızlığı ❌ Yok ⚠️ Kısmi ✅ Tam
Test edilebilirlik ❌ Zor ✅ İyi ✅ İyi
Domain sınırları ❌ Yok ✅ Net ✅ Net
Network gecikmesi ✅ Yok ✅ Yok ❌ Var
Dağıtık transaction ✅ Kolay ✅ Kolay ❌ Karmaşık
Gözlemlenebilirlik ✅ Basit ✅ Basit ❌ Karmaşık
Mikroservise geçiş ❌ Zor ✅ Kolay

Denetim Listesi

✅ Domain sınırları koda dokunmadan önce kağıt üzerinde belirlendi
✅ Her modülün tek bir public interface'i var
✅ Modüller birbirinin internal sınıflarına erişemiyor
✅ Her modül kendi DbContext'ini kullanıyor
✅ Veritabanı şema bazlı ayrıma sahip
✅ Cross-module iletişim event veya interface üzerinden
✅ Shared Kernel domain event sözleşmelerini barındırıyor
✅ InProcessEventBus implemente edildi
✅ Her modül bağımsız test edilebilir
✅ NetArchTest ile mimari kurallar otomatik test ediliyor
✅ Strangler Fig ile kademeli geçiş planlandı
✅ Feature flag ile paralel çalışma destekleniyor

Temel Çıkarımlar

  • Monolith kötü değildir — yönetilemeyen monolith kötüdür
  • Mikroservis her problemin çözümü değildir — getirdiği karmaşıklık çoğu ekip için erkendir
  • Modüler monolith ikisi arasındaki en pragmatik duraktır
  • Domain sınırları teknik değil, iş analiziyle belirlenir
  • Geçiş big bang değil, Strangler Fig ile kademeli olmalıdır
  • Modül sınırları otomatik testlerle korunmazsa zamanla erir
  • Doğru kurulmuş modüler monolith'te mikroservise geçiş sadece IEventBus'ı değiştirmektir

Mimari kararlar geri alınamaz değildir — ama maliyetlidir. Modüler monolith bu maliyeti ertelemez, azaltır. Sınırları bugün doğru çizerseniz yarın yanlış mimariyle savaşmazsınız.