Yeni bir fikirle yola çıkan, tek başına veya küçük bir ekiple bir ürün geliştirmeye çalışan bir “indie hacker” için mimari kararlar kritik önem taşır. Ben de yıllardır kendi yan ürünlerimi geliştirirken, veya büyük kurumsal projelerde çalışırken bu kararların ne kadar belirleyici olduğunu defalarca gördüm. Özellikle de Monolith ve Modular Monolith arasındaki seçim, projenin gelecekteki ölçeklenebilirliği, bakım maliyeti ve geliştirme hızı üzerinde doğrudan etki ediyor. Bu yazıda, bu iki yaklaşımı kendi gözümden, pratik faydaları ve zorlukları üzerinden değerlendireceğim ve bir indie hacker olarak benim genellikle hangi yolu tercih ettiğimi anlatacağım.
Bir projeye başlarken mimari seçimi, genellikle ürünün pazarla buluşma hızını ve ilk iterasyonlardaki çevikliği doğrudan etkiler. Monolith, başlangıçta cazip gelse de, zamanla getirebileceği yükleri iyi hesaplamak gerekiyor. Modular Monolith ise, mikroservislerin getirdiği karmaşıklığa bulaşmadan, daha düzenli ve sürdürülebilir bir yapı kurmanın bir yolunu sunuyor. Benim tecrübelerime göre, bu dengeyi doğru kurmak, özellikle sınırlı kaynaklarla çalışan bir indie hacker için hayati.
Monolith: Hızlı Başlangıç ve Gizli Maliyetler
Monolith mimarisi, tüm uygulama bileşenlerinin tek bir kod tabanında ve tek bir dağıtım birimi olarak bulunması anlamına gelir. Yani, bir web uygulaması düşünün; veritabanı erişimi, iş mantığı, kullanıcı arayüzü, API’ler hepsi aynı proje içinde, çoğu zaman aynı sunucu process’inde çalışır. Bu yaklaşım, özellikle hızlı prototipleme ve ilk ürün (MVP) aşamalarında inanılmaz bir hız sağlar. Birçok yan ürünümü başlangıçta Monolith olarak geliştirdim; çünkü hızlı bir şekilde fikirleri hayata geçirmem gerekiyordu. Tek bir repo, tek bir deploy süreci, tek bir test paketi… Her şey basit ve anlaşılır.
Ancak bu basitliğin zamanla ortaya çıkan gizli maliyetleri var. Bir üretim firmasının ERP’sini ilk geliştirmeye başladığımızda da benzer bir Monolith yapıdaydık. Başlangıçta küçük bir ekiple çok hızlı ilerledik. Ancak sistem büyüdükçe ve özellikler arttıkça, yeni bir özellik eklemek veya mevcut bir hatayı düzeltmek belirgin şekilde uzamaya başladı. Kod tabanı o kadar iç içe geçiyordu ki, bir yerdeki değişiklik başka bir yerde beklenmedik bir hataya yol açabiliyordu. Örneğin sipariş modülündeki küçük bir değişikliğin, faturalama modülünde hatalı indirim hesaplamasına neden olduğu durumlar yaşadık. Bu tür sorunları bulmak ve düzeltmek, kaynaklarımızı ciddi şekilde tüketiyordu.
Özellikle büyük ve kompleks iş süreçleri içeren bir yazılımda Monolith’in dezavantajları daha belirgin hale geliyor. PostgreSQL performansını optimize etmek için index stratejilerini incelerken veya N+1 sorgu problemlerini çözerken, Monolith’teki her yerin birbirine bağımlı olması, refactoring yapmayı veya yeni yaklaşımlar denemeyi zorlaştırıyor. Kendi yan ürünlerimde de aynı örüntüyü gördüm: basit bir Monolith ile başlayıp farklı domainler (örneğin filtreleme mantığı ile kullanıcı verisi yönetimi) büyüdükçe, her yeni özellik eklediğimde kod tabanının ne kadar kırılganlaştığını fark ettim. Bu, beni daha modüler bir yaklaşıma iten önemli bir deneyim oldu.
Modular Monolith: Bir Köprü Çözüm Olarak Gördüğüm
Modular Monolith, adından da anlaşılacağı gibi, Monolith yapısının avantajlarını korurken, içsel olarak modülerliği benimseyen bir yaklaşımdır. Uygulama hala tek bir kod tabanı ve tek bir dağıtım birimi olarak kalır, ancak kod tabanı içerisinde açıkça tanımlanmış, bağımsız modüllere ayrılmıştır. Her modül kendi domain’inden sorumludur ve diğer modüllerle yalnızca tanımlanmış arayüzler (API’ler veya olaylar) aracılığıyla iletişim kurar. Bu, tam bir mikroservis mimarisinin getirdiği operasyonel karmaşıklık (dağıtık transaction’lar, servis keşfi, dağıtık loglama vb.) olmadan, daha temiz ve yönetilebilir bir yapı oluşturmamızı sağlar.
Benim için Modular Monolith, özellikle orta ölçekli veya potansiyel olarak büyüyebilecek indie hacker projeleri için ideal bir köprü çözümdür. Bir müşteri projesinde, üretim planlama ve envanter yönetimini içeren bir sistemi geliştirirken bu yaklaşımı benimsedik. Başlangıçta hızlı bir şekilde ilerlemek istiyorduk ama gelecekte AI ile üretim planlama gibi kompleks modüllerin ekleneceğini biliyorduk. Bu yüzden, “Üretim”, “Envanter”, “Satış” gibi ana domain’leri ayrı Python paketleri veya dizinleri altında yapılandırdık. Her paket kendi PostgreSQL tablolarına sahipti ve diğer paketlerle sadece FastAPI endpoint’leri veya dahili in-memory event bus üzerinden haberleşiyordu.
# Örnek bir Modular Monolith proje yapısı
# my_project/
# ├── src/
# │ ├── __init__.py
# │ ├── main.py # Ana uygulama girişi (FastAPI)
# │ ├── config.py
# │ ├── modules/
# │ │ ├── __init__.py
# │ │ ├── inventory/
# │ │ │ ├── __init__.py
# │ │ │ ├── models.py
# │ │ │ ├── services.py
# │ │ │ ├── api.py # Inventory API endpoint'leri
# │ │ │ └── events.py
# │ │ ├── production/
# │ │ │ ├── __init__.py
# │ │ │ ├── models.py
# │ │ │ ├── services.py
# │ │ │ ├── api.py # Production API endpoint'leri
# │ │ │ └── events.py
# │ │ └── sales/
# │ │ ├── __init__.py
# │ │ ├── models.py
# │ │ ├── services.py
# │ │ ├── api.py # Sales API endpoint'leri
# │ │ └── events.py
# ├── tests/
# └── Dockerfile
Bu yapı sayesinde, “Envanter” modülündeki bir geliştirme, “Üretim” modülünü doğrudan etkilemeden yapılabilir hale geldi. Modüller arasındaki bağımlılıklar azaldığı için kod değişiklikleri daha güvenli hale geldi ve yeni özellik ekleme süresi kısaldı. Ayrıca, gelecekte “Üretim” modülünün aşırı yüklenmesi durumunda, onu bağımsız bir mikroservise dönüştürme seçeneğimiz de saklı kalıyordu, çünkü zaten kendi iç mantığı ve API’si ile izole edilmişti. Bu, özellikle indie hacker’lar için çok değerli; çünkü bugün ne kadar küçük olursa olsun, projenin yarın nereye evrileceğini asla bilemezsiniz.
Trade-off Analizi: Ne Kadar Karmaşıklık, Ne Kadar Esneklik?
Yazılım mimarisi seçimleri her zaman bir trade-off meselesidir. Monolith ve Modular Monolith arasında seçim yaparken de benzer bir durumla karşılaşıyoruz. Benim için anahtar soru şudur: “Şu anki ihtiyaçlarım ve kaynaklarım düşünüldüğünde, ne kadar karmaşıklığı göze alabilirim ve karşılığında ne kadar esneklik elde ederim?”
| Özellik / Mimari Yaklaşım | Monolith | Modular Monolith |
|---|---|---|
| Geliştirme Hızı (Başlangıç) | Çok yüksek, hızlı MVP | Yüksek, Monolith’e yakın |
| Kod Tabanı Yönetimi | Zorlaşır, bağımlılıklar artar | Daha kolay, modüller arası net sınırlar |
| Dağıtım Karmaşıklığı | Düşük, tek bir birim | Düşük, tek bir birim (ancak potansiyel ayırma esnekliği) |
| Ölçeklenebilirlik | Tüm uygulama birlikte ölçeklenir | Modüller kısmen izole, daha esnek ölçeklenme potansiyeli |
| Teknoloji Esnekliği | Düşük, tüm stack aynı | Orta, modüller farklı teknolojilere ayrılabilir |
| Ekip Organizasyonu | Küçük ekipler için uygun, büyüdükçe sorunlu | Modül bazlı ekipler için uygun (daha iyi ayrışma) |
| Refactoring Kolaylığı | Zor | Daha kolay, modül içindeki değişiklikler izole kalır |
Bir indie hacker olarak, genellikle başlangıçta geliştirme hızına öncelik veririm. Pazar testi yapmak, ürün-pazar uyumunu bulmak için hızlıca bir şeyler ortaya koymak önemlidir. Bu yüzden Monolith’in ilk cazibesine kapılmak kolaydır. Ancak deneyimlerim bana gösterdi ki, bu hızın uzun vadeli bir bedeli var. Tek bir codebase içinde farklı algoritmaları ve veri setlerini yönetmek, fonksiyon sayısı arttıkça ve yeni kullanıcı istekleri geldikçe hızla zorlaşır. Özellikle araya AI tabanlı bir tahminleme modülü gibi yeni bir parça eklemeye karar verdiğinizde, mevcut Monolith yapının ne kadar hantal kalabildiğini görürsünüz.
Bu noktada Modular Monolith yaklaşımının bir “kurtarıcı” olabileceğini düşünüyorum. Mevcut Monolith’i baştan parçalara ayırmak yerine, yeni eklenen her özelliği ayrı bir modül olarak tasarlamak daha sağlıklı. Örneğin bir “Tahminleme” modülünü kendi iç bağımlılıkları ve API’si ile izole edersiniz; bu modül ana uygulamanın bir parçası olarak deploy edilir ama kendi içinde bağımsız bir yaşam döngüsüne sahiptir. Bu sayede AI modelini güncellerken veya farklı bir AI provider’ı (Gemini Flash, Groq veya Cerebras gibi) denerken, ana uygulamanın diğer kısımlarını riske atmamış olursunuz. Bu, hem hız hem de geleceğe yönelik esneklik sağlayan bir dengedir.
Uygulama Detayları: Kod Seviyesinde Modulariteyi Sağlamak
Modular Monolith’in kağıt üzerinde harika görünmesi bir yana, asıl mesele bunu kod seviyesinde nasıl uygulayacağınızdır. Bir Monolith’i gerçekten modüler hale getirmek için disiplinli bir yaklaşım gerekir. Benim genelde kullandığım yöntem, projeyi domain’lere göre ayırmak ve bu domain’lerin kendi içinde kapalı birer paket gibi davranmasını sağlamaktır. Python dünyasında bu, ayrı dizinler, __init__.py dosyaları ve açıkça tanımlanmış import kuralları ile yapılabilir.
Örneğin, bir üretim ERP’sinde “Satış”, “Üretim” ve “Muhasebe” gibi ana modüllerim vardı. Her birini ana src/modules dizini altında ayrı birer alt dizin olarak oluşturdum.
# src/modules/sales/api.py
from fastapi import APIRouter
from src.modules.sales.services import create_order, get_order
router = APIRouter(prefix="/sales", tags=["Sales"])
@router.post("/orders/")
async def create_new_order(order_data: dict):
# order_data'yı işleme al, validasyon yap
order = create_order(order_data)
# Dahili bir event fırlat (örneğin, order_created event)
return {"message": "Order created", "order_id": order.id}
# src/modules/production/events.py
from src.core.event_bus import EventBus # Varsayımsal bir in-memory event bus
def handle_order_created(order_id: int):
# Satış modülünden gelen "order_created" event'ini dinle
# Üretim planlamayı tetikle
print(f"Order {order_id} created. Initiating production plan.")
# production_service.plan_production(order_id)
# Ana uygulama (main.py)
from fastapi import FastAPI
from src.modules.sales.api import router as sales_router
from src.modules.production.api import router as production_router
from src.modules.production.events import handle_order_created
from src.core.event_bus import EventBus
app = FastAPI()
app.include_router(sales_router)
app.include_router(production_router)
# Event listener'ları kaydet
EventBus.subscribe("order_created", handle_order_created)
Bu yapıda dikkat edilmesi gerekenler:
- Açık API’ler: Modüller arası iletişim, ya açıkça tanımlanmış
FastAPIendpoint’leri üzerinden ya da birEventBus(genellikle in-memory) üzerinden gerçekleşmeli. - Tek Yönlü Bağımlılıklar (Tercihen): Mümkünse, modüller arasında tek yönlü bağımlılıklar kurmaya çalışırım. Örneğin, “Satış” modülü “Üretim” modülünü direkt çağırmaz, bir olay yayımlar ve “Üretim” modülü bu olayı dinler. Bu, bağımlılık döngülerini engeller.
- Veritabanı Şeması İzolasyonu: Her modülün kendi veritabanı tablolarına sahip olması (prefix ile veya ayrı şemalarla) idealdir. Böylece bir modülün veri şeması değişikliği diğerini daha az etkiler. Farklı iş birimlerinin verilerini ayrı şemalarda tutmak, bu izolasyonu sağlamanın etkili bir yoludur.
- Sıkı Kural Uygulama: Kod incelemelerinde modüller arası kurallara uyulup uyulmadığını kontrol etmek önemlidir. Gerekirse
pre-commithook’ları veya statik analiz araçları ile bu kurallar otomatikleştirilebilir.
Bu yaklaşım, bana hem geliştirme esnekliği sağlıyor hem de gelecekte performans sorunları yaşadığımda veya farklı bir teknolojiye ihtiyaç duyduğumda belirli bir modülü Monolith’ten ayırıp ayrı bir mikroservis olarak deploy etme olanağı tanıyor. Örneğin, AI ile üretim planlama modülü çok fazla kaynak tüketmeye başladığında, onu ayrı bir Kubernetes pod’unda çalıştırmak, diğer ERP bileşenlerini etkilemeden kolayca yapılabilir hale geliyor.
Operasyonel Bakış: Bare-metal ve Container Dünyasında Farklar
Mimari seçimlerimin operasyonel tarafa yansımaları, benim için her zaman öncelikli bir konu olmuştur. Saha tecrübemde, en iyi tasarlanmış sistemlerin bile operasyonel zorluklar yüzünden başarısız olduğunu çok gördüm. Monolith veya Modular Monolith fark etmeksizin, uygulama dağıtım ve yönetimi konusunda belli başlı tercihlerim var.
Ben bare-metal + container hibrit dağıtım yöntemini seven biriyim. Kendi VPS’lerimde veya müşteri projelerinde, genellikle Linux makineler üzerine Docker ve docker-compose kurarak ilerlerim. Monolith bir uygulama için Dockerfile yazmak ve onu docker-compose.yml ile çalıştırmak oldukça basittir. Tek bir servis, tek bir imaj.
# docker-compose.yml (Basit Monolith veya Modular Monolith için)
version: '3.8'
services:
my_app:
build: .
ports:
- "8000:8000"
environment:
DATABASE_URL: "postgresql://user:password@db:5432/mydatabase"
depends_on:
- db
restart: unless-stopped
db:
image: postgres:14-alpine
environment:
POSTGRES_DB: mydatabase
POSTGRES_USER: user
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Bu docker-compose.yml dosyası, hem basit bir Monolith hem de içsel olarak modüler hale getirilmiş bir Modular Monolith için geçerlidir. Uygulama hala tek bir container içinde çalıştığı için operasyonel karmaşıklık artmaz. Nginx reverse proxy kurarak SSL sonlandırma ve statik dosya sunumu gibi işlemleri de my_app container’ından ayırabilirim.
Ancak, Modular Monolith’in gelecekteki operasyonel avantajları burada gizlidir. Eğer “AI ile üretim planlama” modülü gibi belirli bir modül, gelecekte aşırı kaynak tüketmeye başlarsa veya farklı bir runtime ortamına (örneğin GPU gerektiren bir ML altyapısı) ihtiyaç duyarsa, onu ana Monolith’ten ayırıp ayrı bir Docker container’ı olarak deploy etmek çok daha kolay olur. Zaten kendi iç API’leri ve bağımlılıkları izole edilmiş olduğu için, bu ayrıştırma süreci Monolith’e göre çok daha az sancılı geçer. Bu, self-hosted runner ekonomisi gibi alanlarda da benzer düşünce yapısını kullanırım; bir hizmeti ne zaman ayırıp ayrı bir VM’de çalıştırmalıyım veya ne zaman ana makine üzerinde tutmalıyım.
Benim journald loglarını takip ederken veya cgroup memory limit’lerini ayarlarken gördüğüm, tek bir process olarak çalışan uygulamanın yönetimi her zaman daha kolaydır. PostgreSQL connection pool tuning’i yaparken bile, tek bir uygulama instance’ı için ayarları yapmak, dağıtık mikroservisler için her servisin kendi pool’unu yönetmesinden daha az hataya açıktır. Bu yüzden, bir indie hacker olarak operasyonel basitliği her zaman ön planda tutarım.
Geleceğe Yönelik Düşünceler ve Benim Seçimim
Yazılım mimarisi, projenin ömrü boyunca sabit kalacak bir şey değildir; evrilir. Monolith ile başlayıp Modular Monolith’e geçiş yapmak veya Modular Monolith’ten belirli modülleri mikroservislere dönüştürmek, doğal bir büyüme sürecinin parçasıdır. Benim için bu evrimin tetikleyicileri genellikle şunlar olmuştur:
- Ekip Büyüklüğü: Tek başıma veya 2-3 kişilik bir ekiple çalışırken Monolith veya Modular Monolith idealdir. Ancak ekip 10-15 kişiyi aştığında, farklı ekiplerin bağımsız çalışabilmesi için mikroservislere yönelmek daha mantıklı hale gelir.
- Performans Engelleri: Belirli bir modülün (örneğin, benim AI tabanlı planlama modülüm gibi) kaynak tüketimi veya performans gereksinimleri, diğer uygulama bileşenlerini etkilemeye başladığında, onu ayırmak kaçınılmaz olur.
- Teknoloji Çeşitliliği İhtiyacı: Tüm uygulamanın aynı teknoloji yığını ile gitmek zorunda kalması, yenilikleri veya farklı dil/framework’leri denemeyi zorlaştırır. Bir modülün
Rustile yazılması gerektiğinde, onu ayırmak iyi bir seçenektir. - İş Süreçlerinin Karmaşıklığı:
Event-sourcingveyaCQRSgibi gelişmiş mimari desenlerin sadece belirli domain’lerde anlamlı hale geldiği durumlarda, o domain’i izole etmek daha kolaydır.
Benim net pozisyonum, bir indie hacker projesi için Modular Monolith ile başlamaktır. Bu yaklaşım, bana Monolith’in hızlı geliştirme avantajını sunarken, aynı zamanda gelecekteki büyüme ve ölçeklenebilirlik ihtiyaçlarım için sağlam bir temel sağlıyor. Daha önce VPS migration sürecinde benzer bir trade-off yaşamıştım; basitlik mi, esneklik mi? Genellikle basitliği koruyarak esnekliği artırma yolunu seçerim. PostgreSQL index stratejileri veya Redis OOM eviction policy seçimleri gibi teknik detaylarda bile, başlangıçta genel bir çözümle başlayıp, performans regresyonları veya WAL bloat gibi somut sorunlar ortaya çıktığında daha spesifik ayarlamalara giderim. Bu pragmatik yaklaşım, kısıtlı zaman ve kaynaklarla çalışan bir indie hacker için en sürdürülebilir yoldur.
Bir sonraki yazımda, CI/CD reliability konusundaki tecrübelerimi ve blue-green veya canary deploy stratejilerini indie hacker projelerinde nasıl uyguladığımı anlatacağım.