Tek Repo mu Çoklu Repo mu: Monorepo Avantajları ve Riskleri

Yıllar önce bir startup’ta çalışırken repo yapısı konusunda ciddi bir karar vermek zorunda kaldım. Ürün büyüyordu, takım genişliyordu ve her şeyi tek bir repo’da tutmaya devam etmek mi, yoksa servisleri ayrı ayrı repo’lara mı taşımak gerekiyordu? O dönemde verdiğimiz kararın sonuçlarını uzun süre yaşadık, hem iyi hem de kötü taraflarıyla. Bu yazıda monorepo ile multirepo arasındaki farkı teorik değil, gerçek operasyonel deneyimler üzerinden ele alacağım.

Monorepo Nedir, Ne Değildir

Önce kavramı netleştirelim. Monorepo, tüm projelerin, servislerin ve kütüphanelerin tek bir Git repository’sinde tutulduğu yaklaşımdır. Google, Meta, Microsoft gibi şirketler bu modeli onlarca yıldır kullanıyor. Ama burada kritik bir ayrım var: monorepo, monolit anlamına gelmiyor. Tek bir repo içinde onlarca bağımsız servis, uygulama ve paylaşımlı kütüphane bulunabilir. Servisler hâlâ bağımsız deploy edilebilir, bağımsız ölçeklenebilir.

Multirepo ise her projenin, servisin ya da komponentin kendi repository’sinde yaşadığı klasik yaklaşım. Çoğu organizasyon buradan başlar, çünkü doğal ve sezgisel görünür.

Neden Monorepo’ya Geçilir

Bunu anlamak için önce multirepo’nun zamanla nasıl ağrı yarattığını görmek lazım.

Diyelim ki 8 mikroservisiniz var ve bunların hepsi ortak bir utils kütüphanesini kullanıyor. Bu kütüphane ayrı bir repo’da ve NPM/PyPI’a publish ediliyor. utils‘de bir bug bulundu, düzelttiniz, yeni versiyon çıkardınız. Şimdi 8 servisin her birinin repo’suna girip bağımlılığı güncellemeniz, test etmeniz ve deploy etmeniz gerekiyor. Bu operasyon hem zaman alıcı hem de hata üretmeye çok açık.

Ya da şöyle düşünün: bir geliştirici hem backend servisini hem de shared schema kütüphanesini aynı anda değiştirmek zorunda. İki ayrı PR, iki ayrı review süreci, timing koordinasyonu. Bu tür şeyler takımı yavaşlatır.

Monorepo bu senaryolarda şu avantajları sunar:

  • Atomik değişiklikler: Birden fazla servisi etkileyen bir değişiklik tek bir commit ile yapılır
  • Kod paylaşımı kolaylığı: Shared kütüphaneler için publish/versioning overhead’i ortadan kalkar
  • Tek kaynak of truth: Bütün kod tabanı tek bir yerde, arama yapmak, refactor etmek kolaylaşır
  • Tutarlı tooling: Linting, CI/CD, code style tek bir yerden yönetilir
  • Kolay onboarding: Yeni gelen birisi tek bir repo’yu clone’lar, her şey orada

Pratik Monorepo Yapısı Nasıl Görünür

Küçük bir e-ticaret sistemi üzerinden gidelim. Şu an üç servisimiz var: api-gateway, user-service, order-service. Bir de paylaşımlı common paketi.

mkdir ecommerce-monorepo && cd ecommerce-monorepo
git init
mkdir -p services/api-gateway services/user-service services/order-service packages/common

Dizin yapısı şu şekilde olacak:

tree -L 3 ecommerce-monorepo/
# ecommerce-monorepo/
# ├── services/
# │   ├── api-gateway/
# │   ├── user-service/
# │   └── order-service/
# ├── packages/
# │   └── common/
# ├── scripts/
# ├── .github/
# └── Makefile

Python tabanlı servisler için basit bir workspace yapısı kuralım. Root pyproject.toml:

cat > pyproject.toml << 'EOF'
[tool.pytest.ini_options]
testpaths = ["services", "packages"]

[tool.black]
line-length = 88
target-version = ['py311']

[tool.isort]
profile = "black"
EOF

Şimdi shared paketi kuralım:

cat > packages/common/setup.py << 'EOF'
from setuptools import setup, find_packages

setup(
    name="ecommerce-common",
    version="0.1.0",
    packages=find_packages(),
    install_requires=[
        "pydantic>=2.0",
    ],
)
EOF

mkdir -p packages/common/ecommerce_common
cat > packages/common/ecommerce_common/__init__.py << 'EOF'
from .models import BaseResponse, ErrorResponse
from .exceptions import ServiceException

__all__ = ["BaseResponse", "ErrorResponse", "ServiceException"]
EOF

Her servis bu paketi local olarak referans edecek:

# services/user-service/requirements.txt
-e ../../packages/common
fastapi>=0.100.0
uvicorn>=0.23.0

Bu yaklaşımla common paketinde yapılan değişiklik anında tüm servislere yansır, ayrıca publish etmenize gerek kalmaz.

CI/CD’yi Monorepo İçin Ayarlamak

Bu noktada monorepo’nun en kritik operasyonel meselesi ortaya çıkıyor: etkilenen servisleri tespit etmek. 8 servisiniz varsa ve sadece user-service‘de değişiklik yaptıysanız, diğer 7 servisin CI pipeline’ını da tetiklemek istemezsiniz. Hem zaman kaybı hem de kaynak israfı.

GitHub Actions ile bunu şu şekilde çözebilirsiniz:

cat > .github/workflows/ci.yml << 'EOF'
name: Monorepo CI

on:
  push:
    branches: [main, develop]
  pull_request:

jobs:
  detect-changes:
    runs-on: ubuntu-latest
    outputs:
      user-service: ${{ steps.changes.outputs.user-service }}
      order-service: ${{ steps.changes.outputs.order-service }}
      api-gateway: ${{ steps.changes.outputs.api-gateway }}
      common: ${{ steps.changes.outputs.common }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: dorny/paths-filter@v2
        id: changes
        with:
          filters: |
            user-service:
              - 'services/user-service/**'
              - 'packages/common/**'
            order-service:
              - 'services/order-service/**'
              - 'packages/common/**'
            api-gateway:
              - 'services/api-gateway/**'
              - 'packages/common/**'
            common:
              - 'packages/common/**'

  test-user-service:
    needs: detect-changes
    if: needs.detect-changes.outputs.user-service == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run user-service tests
        run: |
          cd services/user-service
          pip install -r requirements.txt
          pytest tests/
EOF

Bu yapıda packages/common değiştiğinde tüm servislerin test pipeline’ı tetiklenir. Mantıklı, çünkü shared kod değişti. Ama user-service değiştiyse sadece o servisin testleri çalışır.

Nx ile Daha Gelişmiş Monorepo Yönetimi

Node.js/TypeScript ekipleri için Nx gerçekten oyun değiştirici bir araç. Build graph analizi yapar, etkilenen projeleri akıllıca tespit eder ve sonuçları cache’ler.

# Nx workspace kurulumu
npx create-nx-workspace@latest myorg --preset=empty
cd myorg

# Servisleri ekleme
nx generate @nx/node:app api-gateway
nx generate @nx/node:app user-service
nx generate @nx/js:lib shared-types

# Sadece etkilenen projeleri build et
nx affected:build --base=main --head=HEAD

# Sadece etkilenen projelerin testlerini çalıştır
nx affected:test --base=main --head=HEAD

Nx’in en güçlü özelliklerinden biri distributed caching. nx.json içinde cloud cache aktif edilirse, bir developer’ın makinasında çalışan build sonucu başkasının makinasında ya da CI’da yeniden kullanılır:

cat > nx.json << 'EOF'
{
  "tasksRunnerOptions": {
    "default": {
      "runner": "nx/tasks-runners/default",
      "options": {
        "cacheableOperations": ["build", "test", "lint", "e2e"]
      }
    }
  },
  "affected": {
    "defaultBase": "main"
  }
}
EOF

Monorepo’nun Gerçek Riskleri

Artık reel konuşalım. Monorepo her derde deva değil. Yıllar içinde gördüğüm gerçek problemler şunlar:

Repo boyutu problemi

Git, büyük binary dosyalarla ve uzun geçmişle iyi başa çıkmaz. Monorepo’nuz büyüdükçe git clone, git status gibi temel operasyonlar yavaşlar. Google’ın neden kendi VCS’ini (Piper) yazdığını düşünün. Bu riski yönetmek için:

# Sığ clone kullanmak (CI ortamlarında önemli)
git clone --depth=1 https://github.com/org/monorepo.git

# Git sparse checkout ile sadece ilgili servisi çekme
git clone --filter=blob:none --sparse https://github.com/org/monorepo.git
cd monorepo
git sparse-checkout set services/user-service packages/common

Kimin neye erişeceği problemi

Multirepo’da erişim kontrolü basittir: hangi repo’ya erişim var, o kadar. Monorepo’da ise farklı takımların farklı dizinlere erişimini yönetmek zorunda kalırsınız. GitHub’ın CODEOWNERS dosyası burada devreye girer:

cat > .github/CODEOWNERS << 'EOF'
# Global owners
* @platform-team

# Service-specific owners
/services/user-service/ @user-team
/services/order-service/ @order-team
/services/api-gateway/ @backend-team
/packages/common/ @platform-team @backend-team

# Infrastructure
/infra/ @devops-team
/.github/ @devops-team @platform-team
EOF

Bu sayede user-service‘e yapılan PR’lar otomatik olarak @user-team‘e review için gider. Herkesin herkese erişimi olsa da PR sürecinde sorumluluk netleşir.

Accidental coupling problemi

Bu bence monorepo’nun en sinsi riski. Kod yakın olunca geliştiriciler servisleri doğrudan birbirine bağlamaya başlar. “Şu modeli buradan import edeyim, zaten aynı repo’da” mantığı servislerin boundary’lerini eroder. Bunu önlemek için mimari kuralları enforce etmek gerekir:

# Nx ile dependency constraints tanımlama
cat > .eslintrc.json << 'EOF'
{
  "rules": {
    "@nx/enforce-module-boundaries": [
      "error",
      {
        "depConstraints": [
          {
            "sourceTag": "scope:user-service",
            "onlyDependOnLibsWithTags": ["scope:shared", "scope:common"]
          },
          {
            "sourceTag": "scope:order-service",
            "onlyDependOnLibsWithTags": ["scope:shared", "scope:common"]
          }
        ]
      }
    ]
  }
}
EOF

Bu kural user-service‘in order-service‘i direkt import etmesini compile-time’da engeller. Boundary’ler kod ile değil, tooling ile korunur.

Büyük refactorlar cazip gelir ama tehlikeli olabilir

Monorepo’da “şu interface’i her yerde değiştireyim” tek bir PR ile mümkün hale gelir. Bu güçlü ama tehlikeli. Özellikle yeterli test coverage yoksa, büyük cross-cutting değişiklikler sessizce probleme neden olabilir. Monorepo geçişiyle birlikte test disiplinini artırmak zorundasınız.

Migration Stratejisi: Multirepo’dan Monorepo’ya Geçiş

Bu geçişi hiç yapan bilir, kolay değil. Ama git history’yi koruyarak yapılabilir. İşte temel yaklaşım:

# Mevcut repo'ları yeni monorepo'ya history'yi koruyarak taşıma
# Kaynak repo: user-service
git clone https://github.com/org/user-service.git
cd user-service

# Tüm dosyaları bir alt dizine taşıyoruz
git filter-repo --to-subdirectory-filter services/user-service

# Monorepo'ya remote ekle ve merge et
cd ../monorepo
git remote add user-service ../user-service
git fetch user-service
git merge --allow-unrelated-histories user-service/main
git remote remove user-service

Bu işlemi her servis için tekrarlarsınız. git filter-repo aracı standart git kurulumunda gelmez, ayrıca install etmeniz gerekir:

pip install git-filter-repo
# ya da
brew install git-filter-repo

Geçiş sonrası eski repo’ları hemen archive etmeyin. En az bir sprint boyunca her iki yapıyı da çalışır tutun. Geçiş sırasında mutlaka karşılaşacağınız şey: bazı CI/CD bağımlılıklarını, secret yönetimini ve environment variable organizasyonunu sıfırdan düşünmek zorunda kalacaksınız.

Hangi Organizasyonlar İçin Hangisi Daha Uygun

Bunu net bir formülle söylemek mümkün değil ama pratik bir rehber çizebilirim:

Monorepo size daha uygun olabilir eğer:

  • Takımlar arası kod paylaşımı yoğunsa
  • Aynı anda birden fazla servisi etkileyen değişiklikler sıkça yapılıyorsa
  • Tutarlı tooling ve standartlar önemliyse
  • Takım boyutu makul düzeyde ve iletişim iyiyse (100-200 kişi altı genellikle iyi çalışır)
  • Platform ekibi var ve monorepo altyapısını aktif olarak yönetebilecek kapasitedeyse

Multirepo size daha uygun olabilir eğer:

  • Farklı takımlar tamamen bağımsız çalışıyorsa ve kod paylaşımı minimumsa
  • Farklı teknoloji stackleri kullanılıyorsa (Go servisi, Python servisi, Ruby servisi birbirinden çok farklı tooling gerektirir)
  • Güçlü bir platform ekibiniz yoksa ve monorepo altyapısını yönetemeyecekseniz
  • Takımlar arası güven ve boundary yönetimi kritik bir organizasyonel sorunsa

Makefile ile Günlük Operasyonları Kolaylaştırmak

Monorepo’da geliştiricilerin hayatını kolaylaştıracak bir Makefile:

cat > Makefile << 'EOF'
.PHONY: help build test lint dev-up dev-down

help:
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "33[36m%-20s33[0m %sn", $$1, $$2}'

build: ## Tüm servisleri build et
	@for service in services/*/; do 
		echo "Building $$service..."; 
		docker build -t $$(basename $$service) $$service; 
	done

test: ## Tüm testleri çalıştır
	@for service in services/*/; do 
		echo "Testing $$service..."; 
		cd $$service && pytest tests/ && cd ../..; 
	done

test-service: ## Belirli bir servisi test et: make test-service SERVICE=user-service
	cd services/$(SERVICE) && pytest tests/ -v

lint: ## Tüm kod tabanında lint çalıştır
	black --check .
	isort --check .
	flake8 services/ packages/

dev-up: ## Tüm servisleri local'de başlat
	docker compose up -d

dev-down: ## Local servisleri durdur
	docker compose down

logs: ## Servis loglarını takip et: make logs SERVICE=user-service
	docker compose logs -f $(SERVICE)
EOF

Bu Makefile her geliştiricinin make help ile ne yapabileceğini anında görmesini sağlar. Monorepo büyüdükçe bu tür operasyonel rehberler kritik hale gelir.

Sonuç

Monorepo vs multirepo tartışması teknik bir tercihten önce organizasyonel bir karar. Doğru araçlarla (Nx, Bazel, Turborepo) ve doğru disiplinle monorepo gerçekten büyük avantajlar sunar: atomik değişiklikler, kolay kod paylaşımı, tek kaynaklı doğruluk. Ama bu avantajlar bedavaya gelmiyor. Scalable CI/CD altyapısı, erişim yönetimi, dependency boundary koruması ve aktif bir platform ekibi gerektiriyor.

En önemli uyarım şu: eğer organizasyonunuzda kod sahipliği ve boundary’ler zaten kültürel olarak iyi yönetilmiyorsa, monorepo bu problemi çözmez, aksine büyütür. Teknik araçlar organizasyonel disiplinin yerini alamaz. Önce kültürü doğru kurun, araç seçimi ona göre şekillensin.

Küçük ve orta ölçekli Türk teknoloji şirketleri için önerim şu yönde: eğer 3-5 servisten fazlasına sahipseniz ve bu servisler gerçekten kod paylaşıyorsa, monorepo’ya geçişi ciddiye alın. Ama geçiş için platform ekibi kapasitesi ayırın ve araçları önceden iyice test edin. Yarım yamalak bir monorepo, iyi yönetilen multirepo’dan çok daha kötüdür.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir