Yeni bir projenin ilk gününde, mimari tahtasının başında biri mutlaka şu soruyu soruyor: “Servisleri nasıl böleceğiz?” Cevabım çoğu zaman aynı: “Şimdilik bölmüyoruz.”
Bu bir erteleme ya da üşengeçlik değil. Neredeyse her yeni projeye bilinçli olarak modüler monolit ile başlıyorum: tek codebase, tek deploy, tek veritabanı — ama içeride net modül sınırları. Bu yazı neden böyle yaptığımın gerekçesi.
”Modüler monolit” ne demek?
Üç şeyi birbirinden ayırmak gerekiyor:
- Klasik monolit — sınır yok. Her şey her şeye erişir, zamanla “big ball of mud”.
- Mikroservisler — sınır var, ama sınır aynı zamanda bir network çağrısı.
- Modüler monolit — sınır var, ama sınır process içinde; bir method çağrısı.
Asıl mesele şu: modül sınırı çizmek ile onu ayrı bir servise koymak iki ayrı karardır. Mikroservis mimarisi bu ikisini tek bir karara sıkıştırıyor — sınır demek, otomatik olarak ayrı deploy, ayrı veritabanı, network demek. Modüler monolit bu ikisini ayırıyor: sınırı bugün çiz, dağıtımı gerçekten gerekince yap.
Sınırlar kodda nasıl görünüyor
Laravel tarafında kullandığım yerleşim kabaca şu:
app/
└── Modules/
├── Billing/
│ ├── Domain/ # entity'ler, value object'ler — modüle özel
│ ├── Application/ # use-case'ler
│ ├── Infrastructure/ # repository, dış servis adaptörleri
│ └── BillingApi.php # modülün TEK public yüzeyi
├── Catalog/
└── Notification/
Kural basit: bir modül, başka bir modülün yalnızca *Api.php sınıfını çağırır. Domain ve Infrastructure içindeki her şey o modüle özeldir — dışarıdan görünmez.
İkinci kural veritabanında: bir modül, başka bir modülün tablosuna doğrudan SELECT atmaz, foreign key bağlamaz. Catalog’un bir ürüne ihtiyacı varsa BillingApi’den geçer, billing_invoices tablosuna el uzatmaz. Bu disiplin sıkıcı görünüyor — değeri tam da burada.
Neden buradan başlıyorum
Beş somut sebep:
-
Sınırı yanlış çizme maliyeti düşük. Projenin ilk haftasında domain’i tam anlamıyorsunuz. Modül sınırı yanlış çıkarsa, modüler monolitte bunu düzeltmek bir refactor — IDE’nin “move” komutu ve birkaç saat. Aynı hatayı mikroserviste düzeltmek iki servisin API kontratını, iki deploy pipeline’ını ve canlı veri taşımayı içeriyor. Erken dönemde sınırlar kesinlikle yanlış olacak; ucuz düzeltilebilen yapıyı seçin.
-
Tek transaction. İki modülü tek bir DB transaction’ı içinde tutarlı güncelleyebiliyorum. Mikroserviste aynı şey saga, outbox, eventual consistency demek. Bunlar gerçek araçlar — ama gerçekten gerekene kadar ertelenmesi gereken bir karmaşıklık.
-
Operasyonel yük neredeyse sıfır. Tek deploy, tek log akışı, izlenecek tek hedef. Servisler arası retry, distributed tracing, kontrat versiyonlama — bunların hiçbiri yok. Üç kişilik bir ekip için bu, haftada kazanılan günlerdir.
-
Refactor araçları çalışıyor. “Rename”, “find usages”, statik analiz — hepsi process sınırı içinde işliyor. Bir method çağrısını compiler doğruluyor; bir network kontratını kimse doğrulamıyor.
-
Mikroservise geçişi kolaylaştırıyor. En sık atlanan nokta bu: sınırlar zaten net olduğu için, bir modülü ayrı servise çıkarmak ileride mekanik bir iş haline geliyor. Modüler monolit, mikroservislerin alternatifi değil; doğru yapıldığında onlara giden en ucuz yol.
Sınırı ayakta tutan disiplin
Modüler monolitin tek zayıf noktası şu: sınırlar zorla uygulanmıyorsa, yapı sessizce eski tip monolite çürür. “Sadece bu sefer” diye atılan her doğrudan çağrı bir delik açar.
Sınırı ayakta tutan dört pratik:
- Statik bağımlılık kuralları.
deptracya da benzeri bir araçla “Catalog, Billing’inDomainkatmanına erişemez” kuralını CI’da zorunlu kılın. İhlal, build’i kırsın. - Tek public yüzey. Her modülün dışarı açık API’si tek bir sınıf; geri kalan her şey internal. Görünürlüğü dilin izin verdiği ölçüde daraltın.
- Veri sınırı. Modüller arası ilişki foreign key ile değil, ID + public çağrı ile kurulur.
- Review’da tek soru. Kod incelemesinde sorulacak en önemli soru: “Bu değişiklik bir modül sınırını izinsiz geçiyor mu?”
Sınır CI tarafından zorlanmıyorsa, elinizde modüler monolit değil, sadece klasör isimleri düzgün bir monolit var. Bütün değer disiplinde.
Ne zaman ayrı servise bölüyorum?
Modülü ayrı bir servise çıkarmak için ölçülmüş bir sebep beklerim. Dört sinyalden biri yeterli:
- Ölçeklenme profili ayrışıyor. Notification modülü CPU-bound, geri kalan sistem I/O-bound. Birini diğerinden bağımsız ölçeklemek istiyorsam, ayırmak artık karşılığını veriyor.
- Sahiplik ayrışıyor. Modül ayrı bir ekibin sorumluluğuna geçti ve bağımsız deploy artık teknik değil, organizasyonel bir ihtiyaç.
- Farklı bir runtime gerekiyor. Ağır bir hesaplama Go’da çok daha ucuz, geri kalan PHP’de kalmalı.
- Hata izolasyonu zorunlu. Bir modülün çökmesi gerçekten geri kalan sistemi düşürmemeli ve bunu process içinde garanti edemiyorum.
Bunlardan biri ölçülerek ortaya çıkana kadar bölmek erken optimizasyondur. Bu, boring architecture savunmamın doğrudan uzantısı: karmaşıklığı, onu zorlayan kanıt gelene kadar ertele. Sınırlar baştan net olduğu için, o gün geldiğinde bölme işi korkutucu bir göç değil, planlı bir adım oluyor.
Mikroservisler bir mimari tarzı değil, bir dağıtım kararıdır. O kararı projenin ilk gününde — domain hakkında en az şey bildiğiniz anda — vermek için hiçbir sebep yok. Modüler monolit bana o kararı erteleme, ve zamanı geldiğinde hazır olma lüksünü veriyor.
Sınırları erken çizin, dağıtımı geç yapın.