Sertaç Yıldırım saha notları

Ana sayfa → Teknik

Monorepo: Kod Yapısı Değil, Ekip Yapısı

Çarşamba 10:15. Ortak kütüphanede tek satır: hata kodu enum’una yeni bir değer. Yazması 20 dakika. Sonra sırayla: kütüphaneye versiyon bas, 8 servis reposunda bump PR’ı aç, 8 CI bekle, 8 review, 8 deploy. Sonuncusu Cuma 16:30’da canlıdaydı. Ekip 3 kişi. Kimse kimseyi beklemiyor; repolar birbirini bekliyor.

Özet
  • Repo sınırı, ekip sınırıdır. 4 ekip 14 kişiyken 9 repo doğruydu: kimse kimsenin PR’ını beklemesin diye. Ekip 3 kişiye inince aynı sınır, engellemediği kimse için bedel almaya başladı.
  • Ortak kütüphanede 20 dakikalık iş, 3 günlük teslim. 8 bump PR, 8 CI, 8 review; bir servis unutuldu. Monorepoda aynı iş tek PR, tek review, tek commit.
  • Monorepo ≠ monolit. Tek repo, ama her servis ayrı deploy edilir. Ortak lib değişince 8 servisi birden çıkarmak zorundaysan sorun repoda değil, kütüphanenin sınırında.
  • Önce CI’ı böl, sonra kodu taşı. Path filtresi olmadan taşırsan her commit her şeyi tetikler; altı ay sonra pipeline 25 dakikadır ve yine repo bölmeyi konuşursunuz.
  • Geçmişi taşı, kopyalama. git filter-repo ile; yoksa iki yıllık git blame gider.
  • Asıl soru servis sayısı. 3 kişiye 8 servis; 8 pipeline, 8 dashboard, 8 alarm seti demek. Repo birleştirmek servis sayısını azaltmaz; taşıma, hep birlikte değişen servisleri birleştirmek için fırsat.

Repo sınırı neyi çözer?

Projeye üç yıl önce 4 ekip, 14 kişiyle başladık: ödeme, hesap, raporlama, entegrasyon. Her ekibin kendi servisleri, her servisin kendi reposu; ortada bir common kütüphanesi. 9 repo. O gün için doğru karardı ve sebepleri somuttu:

  • Ödeme ekibinin PR’ı, raporlama ekibinin yarım kalmış işini beklemiyordu.
  • CI küçük ve hızlıydı: repo başına 4 dakika.
  • Entegrasyon reposunu dışarıdan bir ajans yazıyordu; yalnızca o repoyu görüyordu.
  • Release ritmi farklıydı: ödeme haftada 3 çıkıyor, raporlama ayda 1.

Üç yılda ekip küçüldü. Bugün 3 kişiyiz ve aynı 8 servisi işletiyoruz. Repolar olduğu yerde duruyor; sebepleri durmuyor.

Poly repo’nun verdiği şey14 kişiyken3 kişiyken
Bağımsız review4 ekip birbirini beklemiyorZaten aynı 3 kişi review yapıyor
Ayrı erişimDış ajans tek repo görüyorDış ajans yok
Küçük, hızlı CI4 dk4 dk — ama 8 kez
Ayrı release ritmiÖdeme haftada 3, rapor ayda 1Hepsini aynı kişi çıkarıyor
Bedeli: ortak kod koordinasyonu4 ekibe yayılıyorEkibin tamamına biniyor
Repo sınırı, ekip sınırını yansıtır. Ekip tekse repo da tek olabilir.

Sahadan: bir enum değeri, üç gün

Girişteki olayın dökümü. Yeni bir limit kuralı için ErrorCode enum’una tek değer eklenecek: LimitExceeded. Değişiklik common’da; tüketen 8 servis var.

ZamanNe olduMaliyet
Çarşamba 10:15common’da enum değeri eklendi, PR açıldı20 dk
Çarşamba 11:00Review, merge, 1.14.0 tag, paket yayınlandı45 dk
Çarşamba 11:30–17:008 serviste bump PR (1.13 → 1.14). İkisinde derleme hatası: eski bir switch yeni değeri tanımıyor8 PR, 2 düzeltme
Perşembe8 CI (biri flaky, iki tekrar), 8 review, 5 deployBir kişinin günü
Cuma 16:30Kalan 3 deploy. Raporlama servisi hâlâ 1.13’te: o hafta kimse dokunmadı, bump unutuldu7/8

Kimse hata yapmadı. Süreç, 4 ekip birbirini engellemesin diye tasarlanmıştı; 3 kişilik ekipte engellenecek kimse kalmayınca geriye yalnızca süreç kaldı. Üstüne bir de versiyon matrisi: Cuma akşamı “hangi servis hangi common’da?” sorusunun cevabı bir tabloya bakmayı gerektiriyordu. 5 servis 1.14, 2 servis 1.13, 1 servis hâlâ 1.11. Kimse ezbere bilmiyordu.

Engellediği kimse kalmayan bir sınırın tek işlevi, bedel almaktır.

Karar: monorepo — ama monolit değil

Karar tek repo. Ne değişiyor, ne değişmiyor; ikisini de baştan yazdık, çünkü “monorepo” kelimesi ekipte iki farklı şey çağrıştırıyordu.

Değişen
  • Tek repo: services/ altında 8 servis, libs/ altında ortak kod.
  • common paket değil, kaynak olarak referanslanır. Versiyon matrisi biter.
  • Ortak kod değişikliği + onu kullanan servisler tek PR, tek review, tek commit. Atomik refactor mümkün: rename, interface değişikliği.
  • Yeni gelen biri git clone ile her şeyi görür.
Değişmeyen
  • Her servisin kendi Dockerfile’ı, kendi pipeline’ı, kendi deploy’u.
  • Servisler çalışma zamanında birbirini nasıl çağırıyorsa öyle çağırmaya devam eder.
  • Bir servisin hatası diğerinin deploy’unu geri almaz.
  • “Hepsini birden deploy et” diye bir buton yok. Olmayacak.

Sağdaki liste soldakinden önemli. Monorepoya geçip common değişince 8 servisi birden deploy etmek zorunda kalıyorsan bir şey kazanmadın; sorun repoda değil, kütüphanenin sınırındaydı. Poly repo bunu versiyon matrisinin arkasına saklıyordu. Monorepo saklamaz: her PR’da kaç servis dizinine dokunduğun görünür. O sayı sürekli 8/8 ise ortak kütüphane fazla şey biliyor.

Monorepo, ortak kütüphanenin sınır hatasını çözmez; gösterir.

Nerede batar: dört tuzak

1. Her commit her şeyi tetikler

İlk denemede tek pipeline yazdık: push gelince 8 servisi build et, test et. 4 dakikalık CI 31 dakika oldu, ikinci günde. Çözüm, değişen dizine göre tetiklemek; her servisin kendi workflow’u var ve yalnızca kendi dizini ya da bağımlı olduğu ortak kod değişince çalışıyor. Nx, Turborepo, Bazel gerekmedi; GitHub Actions’ın paths filtresi yetti.

Servis başına workflow, path filtresiyle
# .github/workflows/odeme.yml
name: odeme
on:
  push:
    branches: [main]
    paths:
      - "services/odeme/**"
      - "libs/common/**"        # ortak kod degisince bu servis de test edilir
      - ".github/workflows/odeme.yml"
  pull_request:
    paths:
      - "services/odeme/**"
      - "libs/common/**"

jobs:
  build:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: services/odeme
    steps:
      - uses: actions/checkout@v4
      - run: dotnet test          # sadece bu servis; 4 dk, eskisi gibi

Path filtresi bir bağımlılık grafiğinin en basit hâli: libs/common değişince ona bağımlı 8 workflow çalışır, services/odeme değişince yalnızca biri. Servis sayısı 30’u geçince bu elle yönetilmez ve o gün Nx ya da Bazel konuşulur. 8 serviste konuşulmaz.

2. Paket basmaya devam etmek

Kodu taşıyıp common’ı hâlâ internal NuGet paketi olarak basıyorsan hiçbir şey kazanmadın: bump PR’ları aynı repoda açılır, o kadar. Referans kaynak olmalı.

Önce: paket, versiyon matrisi
<!-- services/odeme/Odeme.csproj -->
<PackageReference Include="Company.Common" Version="1.13.0" />
<!-- 8 serviste 8 farkli satir; hangisi guncel, kimse bilmiyor -->
Sonra: kaynak, her zaman HEAD
<!-- services/odeme/Odeme.csproj -->
<ProjectReference Include="../../libs/common/Common.csproj" />
<!-- common degisince derlenmeyen servis, PR'da kirmizi yanar; Cuma'ya kalmaz -->

Girişteki iki derleme hatası burada ortaya çıkardı: PR’ın içinde, Çarşamba 10:35’te. Unutulan raporlama servisi de unutulamazdı; derlenmezdi.

3. Geçmişi kopyala-yapıştırla taşımak

En ucuz görünen yol: eski repoyu klonla, dosyaları yeni dizine kopyala, tek commit. Bedeli üç ay sonra ödenir: git blame her satır için aynı commit’i gösterir, “bu neden böyle yazılmış” sorusunun iki yıllık cevabı gider. Geçmişi yollar yeniden yazılarak taşınır:

Her eski repo için, geçmişiyle birlikte
# 1) eski repoyu temiz klonla (filter-repo taze klon ister)
git clone --no-local git@github.com:company/odeme-service.git /tmp/odeme
cd /tmp/odeme

# 2) butun gecmiste yollari services/odeme/ altina tasi
git filter-repo --to-subdirectory-filter services/odeme

# 3) monorepoya cek; iki gecmisin ortak atasi yok, izin ver
cd ~/monorepo
git remote add odeme /tmp/odeme
git fetch odeme
git merge --allow-unrelated-histories odeme/main -m "odeme servisini tasi (gecmisiyle)"
git remote remove odeme

# kontrol: eski commit'ler yeni yolda gorunuyor mu?
git log --oneline --follow -- services/odeme/Odeme.csproj | tail -3

Servis başına 15 dakika. Eski repolar silinmez; salt okunur arşive alınır. Bir link, bir CI referansı, bir eski doküman hâlâ oraya bakıyor olabilir.

4. Tek kapı, tek anahtar

Poly repo’nun sessiz faydası erişim kontrolüydü: ajans yalnızca entegrasyon reposunu görüyordu. Monorepoda herkes her şeyi görür. Bugün 3 kişiyiz, sorun değil. Yarın dışarıdan biri girerse CODEOWNERS review’ı sınırlar ama okumayı sınırlamaz. Bunu bugün çözmedik; not olarak yazdık. Çözülmesi gereken gün bellidir: ilk dış hesap açıldığı gün.

Asıl soru: 3 kişi kaç servis?

Repo sayısını konuşurken asıl sayıyı atlıyorduk. 3 kişi, 8 servis. Bu şu demek: 8 pipeline, 8 dashboard, 8 alarm seti, güvenlik yaması çıkınca 8 dependency update. Servis sınırı da repo sınırı gibi ekip sınırıdır; 4 ekip için çizilmiş 8 sınır, 1 ekip için fazladır.

Hangilerini birleştireceğimize göz kararıyla değil, geçmişe bakarak karar veriyoruz. Monorepo burada beklemediğimiz bir şey verdi: geçmiş tek yerde olunca “hangi servisler hep birlikte değişiyor?” sorusu tek komut oldu. Poly repo’da bu soruyu sormak için 8 repodan tarih birleştirmek gerekiyordu; kimse sormamıştı.

Son 6 ay: hangi servis çiftleri aynı commit’te değişti?
git log --since="6 months ago" --pretty=format:'%H' --name-only \
  | awk '
      function flush() {
        for (i = 1; i <= n; i++)
          for (j = i + 1; j <= n; j++)
            pair[s[i] " + " s[j]]++
        n = 0; delete seen
      }
      /^[0-9a-f]{40}$/ { flush(); next }          # yeni commit
      /^services\//    { split($0, p, "/"); d = p[2]
                         if (!(d in seen)) { seen[d] = 1; s[++n] = d } }
      END { flush(); for (k in pair) print pair[k], k }
    ' | sort -rn | head -5

# cikti (bizde):
#   41 hesap + limit
#   23 odeme + hesap
#    9 odeme + bildirim
#    2 raporlama + hesap
#    0 entegrasyon + (herhangi biri)

Sayılar iki şey söylüyor. hesap ile limit altı ayda 41 kez aynı commit’te değişmiş; bu iki servis değil, ağ üstünden konuşan tek servis. Birleştirme adayı bir numara. entegrasyon altı ayda kimseyle birlikte değişmemiş; ayrı kalır, dokunulmaz. Birleştirme, geçişle aynı PR’da yapılmaz; önce taşı, sonra ölç, sonra birleştir.

Üç kişiye sekiz servis, sekiz alarm seti demek. Repo birleştirmek bunu değiştirmez.

Geçiş sırası

Bu sırayla
  1. Boş monorepoda CI’ı kur. Path filtresi, servis başına workflow, bağımsız deploy. Tek servisle dene.
  2. Ortak kütüphaneleri taşı. En çok acı orada, en hızlı kazanım orada.
  3. Servisleri en az bağımlıdan başlayarak taşı. Her taşımada paket referansı → proje referansı. Servis başına bir PR.
  4. Co-change analizi çalıştır. Birleştirme adaylarını listele; birleştirme ayrı iş.
  5. Eski repoları arşivle. Silme.
Bunları yapma
  • Hepsini bir hafta sonunda taşımak. Pazartesi hiçbir CI çalışmaz.
  • “CI’ı sonra düzeltiriz.” Sonra, 31 dakikalık pipeline demek.
  • Paket basmaya devam etmek.
  • Servis birleştirmeyi taşımayla aynı PR’da yapmak. İki değişiklik, tek geri alma noktası.
  • Eski repoları silmek.

Ne izlemeli?

  • Ortak değişiklik lead time. libs/common’da PR açılmasından son servisin deploy’una. Bizde 3 gün; hedef aynı gün.
  • PR başına dokunulan servis dizini. Sürekli 8/8 ise ortak kütüphane fazla şey biliyor; sınırı düzelt.
  • Servis başına CI süresi, p95. Path filtresi çalışıyorsa 4 dakikada kalır. 10’u geçerse birileri filtreyi delmiştir.
  • Haftalık deploy sayısı. Düşüyorsa monorepo korkutuyor demektir; “bir şeyi bozar mıyım” hissi büyümüştür.
  • Versiyon matrisi. Sıfır satır olmalı. Bir satır bile varsa biri paket basıyor.

Kontrol listesi

Geçmeden önce
  • Repo sınırı bugün hangi ekibi hangi ekipten koruyor? Cevap “kimseyi” ise geç.
  • Son ortak kütüphane değişikliği kaç PR, kaç gün sürdü? Kaç servis unutuldu?
  • Her servisin kendi workflow’u ve path filtresi var mı? libs/ değişince hangi workflow’lar çalışıyor?
  • Ortak kod paket olarak mı, kaynak olarak mı referanslanıyor?
  • Geçmiş filter-repo ile mi taşındı? git log --follow eski commit’leri görüyor mu?
  • Kaç kişi kaç servis işletiyor? Hangi çiftler son 6 ayda birlikte değişti?
  • Dışarıdan biri repoya girerse ne görür? O gün için not var mı?
  • Eski repolar arşivlendi mi, silindi mi?

Sonuç

Üç yıl önce 9 repo doğru karardı; bugün de doğru bir kararı geri almıyoruz, kararın sebebi ortadan kalktı. Repo sınırı, 4 ekibin birbirini beklememesi için vardı. 3 kişilik ekipte bekleyecek kimse yok; sınır olduğu yerde durdu ve enum’a eklenen tek değer üç güne mal oldu, bir servis de yolda unutuldu.

Monorepo bu bedeli kaldırıyor; başka bir şeyi de gizlemiyor: ortak kütüphane fazla şey biliyorsa artık her PR’da görünür, 8 servis 3 kişiye fazlaysa git log söyler. İkisini de düzeltmek ayrı iş; ama ikisini de ancak tek repoda ölçebildik.

Akılda kalacak cümle şu: repo yapısı bir mimari tercih değil, ekip yapısının yansımasıdır. Ekip değiştiyse repo da değişir. Değişmiyorsa, bedelini kim ödüyor diye bak.