Modüler Monolith Mimarisi: Clean, Bounded, Scalable

Hadi biraz kafa yoralım. 🧠
Bu mimari doküman, mükemmel Clean Architecture prensiplerine sahip bir modüler monolith için sağlam bir temel oluşturuyor.
Şimdi bunu rehber niteliğinde, herkesin anlayabileceği ama derinlemesine teknik bir blog yazısına dönüştürelim.
Aşağıdaki yazı:
- 👷♂️ Gerçek bir modüler monolith nasıl inşa edilir?
- 🧩 Her modül kendi “mini-core”una nasıl sahip olur?
- 🏗️ Katmanlar nasıl ayrılır, ama sistem tek bir bütün olarak nasıl kalır?
- ⚙️ BufferedLogger, Scheduler, Redis, Kafka gibi altyapılar modüler düzende nasıl yer alır?
Tüm bu soruların cevabını verir.
Hazırsan başlayalım 👇
🧱 Modüler Monolith Mimarisi: Clean, Bounded, Scalable
“Monolith ölmedi. Sadece modülerleşti.”
— Kamil Grzybek, Modular Monoliths with DDD
🎯 1️⃣ Neden Modüler Monolith?
Birçok ekip Microservice’lere geçmeden önce Clean Architecture + Modüler Monolith kombinasyonunu tercih ediyor.
Çünkü:
| Mimari | Avantaj | Dezavantaj |
|---|---|---|
| Monolith | Basit, tek deploy | Kod karmaşası, domain çakışması |
| Microservices | Bağımsız ölçeklenebilirlik | Operasyonel karmaşa |
| Modular Monolith | Tek deploy, bağımsız domainler | ⚡ en iyi denge noktası |
🧩 2️⃣ Temel Felsefe
“Her modül kendi küçük Clean Architecture Core’una sahiptir.”
Yani:
- Her modülün Domain ve Application katmanı kendi içindedir.
- Infrastructure katmanı ortaktır ama her modülün Interface’i üzerinden erişir.
- Presentation (API) katmanı modülleri sadece Application arabirimlerinden çağırır.
- Cross-cutting concerns (auth, logging, caching) gibi bileşenler
SharedveyaBuildingBlocksiçinde yaşar.
🧠 3️⃣ Genel Yapı
src/
├── BuildingBlocks/
│ ├── Shared/
│ │ ├── Abstractions/
│ │ ├── Logging/
│ │ ├── Messaging/
│ │ └── Time/
│ └── Infrastructure/
│ ├── Outbox/
│ ├── EventBus/
│ ├── Redis/
│ └── Kafka/
│
├── Modules/
│ ├── Quiz/
│ │ ├── Quiz.Domain/
│ │ ├── Quiz.Application/
│ │ └── Quiz.Infrastructure/
│ │
│ ├── Scheduler/
│ │ ├── Scheduler.Domain/
│ │ ├── Scheduler.Application/
│ │ └── Scheduler.Infrastructure/
│ │
│ ├── Logging/
│ │ ├── Logging.Domain/
│ │ ├── Logging.Application/
│ │ └── Logging.Infrastructure/
│
└── Api/
├── Program.cs
├── ModulesRegistration.cs
└── Controllers/🧭 Her modül kendi bounded context’ine sahip:
Quiz→ quiz oluşturma, başlatma, sonuç toplamaScheduler→ Redis/Kafka zamanlayıcıLogging→ BufferedLogger servisleri
🧩 4️⃣ Katmanların Rolü (Her Modül İçin)
| Katman | Sorumluluk | Örnek |
|---|---|---|
| Domain | Saf iş kuralları, entity, aggregate | Quiz, Question, QuizScheduledEvent |
| Application | Use case’ler, handler’lar | ScheduleQuizHandler, StartQuizHandler |
| Infrastructure | Veri erişimi, dış servis bağlantıları | RedisScheduler, EventStoreQuizRepository |
Domain (örnek)
// Modules/Quiz/Quiz.Domain/Entities/Quiz.cs
public class Quiz : AggregateRoot
{
public Guid Id { get; private set; }
public string Title { get; private set; }
private readonly List<Question> _questions = new();
public void Schedule(DateTime scheduledAtUtc, IClock clock)
{
if (scheduledAtUtc < clock.UtcNow)
throw new InvalidOperationException("Cannot schedule quiz in the past.");
AddDomainEvent(new QuizScheduledEvent(Id, scheduledAtUtc));
}
}Application (örnek)
// Modules/Quiz/Quiz.Application/UseCases/ScheduleQuiz/ScheduleQuizHandler.cs
public class ScheduleQuizHandler : ICommandHandler<ScheduleQuizCommand>
{
private readonly IQuizRepository _quizRepository;
private readonly IScheduler _scheduler;
private readonly IBufferedLogger _logger;
public async Task Handle(ScheduleQuizCommand command, CancellationToken ct)
{
var quiz = await _quizRepository.GetAsync(command.QuizId);
quiz.Schedule(command.ScheduledAtUtc, SystemClock.Instance);
await _scheduler.ScheduleAsync(command.QuizId, command.ScheduledAtUtc);
await _quizRepository.SaveAsync(quiz);
_logger.Enqueue(new LogEvent($"Quiz {command.QuizId} scheduled at {command.ScheduledAtUtc}"));
}
}Infrastructure (örnek)
// Modules/Quiz/Quiz.Infrastructure/Repositories/QuizRepository.cs
public class QuizRepository : IQuizRepository
{
private readonly EventStoreContext _context;
public QuizRepository(EventStoreContext context) => _context = context;
public Task<Quiz> GetAsync(Guid id) => _context.Aggregates.FindAsync<Quiz>(id);
public Task SaveAsync(Quiz quiz) => _context.SaveAsync(quiz);
}🧱 5️⃣ Bağımlılık Yönetimi
🎯 “Katmanlar bağımlı değil, abstraction’lara bağımlı olmalı.”
Örnek referans yönü (projeler arası):
Quiz.Application → Quiz.Domain
Quiz.Infrastructure → Quiz.Application (interfaces)
Api → Quiz.Application (commands)Infrastructure, Application’da tanımlanmış arabirimleri uygular:
// Application katmanında
public interface IQuizRepository
{
Task<Quiz> GetAsync(Guid id);
Task SaveAsync(Quiz quiz);
}Infrastructure katmanında implement edilir:
public class QuizRepository : IQuizRepository { ... }🧩 6️⃣ Paylaşılan Katmanlar
BuildingBlocks/Shared
IBufferedLogger,IClock,IEventBus,IScheduler- Ortak
Result<T>,PaginatedList<T>gibi generic yapılar
BuildingBlocks/Infrastructure
Gerçek implementasyonlar:
RedisSchedulerKafkaEventBusSystemClockBufferedLoggerService
Her modül bu BuildingBlocks’taki abstraction’ları kullanır.
⚙️ 7️⃣ Modül Kaydı (Composition Root)
Tüm modüller API projesi tarafından runtime’da yüklenir.
Startup’ta modüller register edilir:
// Api/ModulesRegistration.cs
public static class ModulesRegistration
{
public static IServiceCollection AddModules(this IServiceCollection services, IConfiguration config)
{
services.AddQuizModule(config);
services.AddSchedulerModule(config);
services.AddLoggingModule(config);
return services;
}
}Her modül kendi bağımlılıklarını tanımlar:
// Modules/Quiz/Quiz.Infrastructure/DependencyInjection.cs
public static class QuizModule
{
public static IServiceCollection AddQuizModule(this IServiceCollection services, IConfiguration config)
{
services.AddScoped<IQuizRepository, QuizRepository>();
services.AddScoped<IQuizService, QuizService>();
return services;
}
}🧠 8️⃣ Database & Transaction Yönetimi
“Tek database, ama her modül kendi şemasında yaşar.”
Önerilen yapı:
- Tek SQL Server veya PostgreSQL
- Her modül kendi şeması (
quiz,scheduler,logging) - Transaction’lar Application servis seviyesinde yönetilir.
- Outbox pattern kullanılır (Event Store ile tutarlılık için).
🧩 9️⃣ Cross-cutting Concerns (Logging, Caching, Auth)
BufferedLogger (örnek entegrasyon)
IBufferedLogger.Enqueue()Application seviyesinde çağrılır.- Gerçek log işlemi
BufferedLoggerServicetarafından background’da yapılır. - Hiçbir modül doğrudan
ILoggerkullanmaz.
Scheduler (örnek)
IScheduler.ScheduleAsync()Application katmanında çağrılır.- Redis veya Kafka implementasyonu sadece Infrastructure’da.
🚀 10️⃣ Modüler Monolith + Clean Architecture = DDD için Mükemmel Zemin
| Özellik | Açıklama |
|---|---|
| DDD uyumlu | Her modül kendi bounded context’ine sahip |
| Bağımsız geliştirme | Modüller birbirinden izole |
| Test edilebilir | Domain/Application bağımsız |
| Modüler büyüme | Mikroservise geçiş kolay |
| Performant | Tek deploy, minimal network hop |
| Observability-ready | BufferedLogger, Prometheus, OTEL entegre |
🧩 11️⃣ Geleceğe Hazırlık: Microservice Geçişi
Modular Monolith → Microservice geçişi sadece deployment sınırı değiştirir, domain sınırı değil.
| Durum | Modular Monolith | Microservice |
|---|---|---|
| Deployment | Tek binary | Her modül ayrı container |
| Communication | In-process | gRPC / HTTP / Message Bus |
| State | Tek DB, farklı schema | Ayrı DB per service |
| Logging | Shared (BufferedLogger) | Distributed logging |
Bu nedenle her modül, potansiyel olarak kendi microservice’ine dönüşebilir.
📘 12️⃣ Özet
| Katman | İçerik | Scope |
|---|---|---|
| Core (Domain + Application) | İş mantığı, use case’ler | Her modül için ayrı |
| Infrastructure | Teknik detaylar | Paylaşılan |
| BuildingBlocks | Cross-cutting abstractions | Paylaşılan |
| Presentation (API) | HTTP endpoints | Ortak giriş noktası |
🧭 13️⃣ Sonuç
Modüler Monolith:
- Clean Architecture’ın bağımlılık kurallarını uygular,
- Her modülü bir bounded context olarak ele alır,
- Geliştirme hızını artırır,
- Gelecekte Microservices dönüşümünü sorunsuz hale getirir.
Ve bu tarz projelerde senin BufferedLogger mimarin, Event Sourcing + CQRS + Redis Scheduler yapısıyla birleştiğinde,
aslında modern bir Cloud-Native Modular Monolith elde ediyorsun —
yani tek binary ama microservice mimarisi kadar güçlü bir sistem.





