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.
- 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-repoile; yoksa iki yıllıkgit blamegider. - 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 şey | 14 kişiyken | 3 kişiyken |
|---|---|---|
| Bağımsız review | 4 ekip birbirini beklemiyor | Zaten aynı 3 kişi review yapıyor |
| Ayrı erişim | Dış ajans tek repo görüyor | Dış ajans yok |
| Küçük, hızlı CI | 4 dk | 4 dk — ama 8 kez |
| Ayrı release ritmi | Ödeme haftada 3, rapor ayda 1 | Hepsini aynı kişi çıkarıyor |
| Bedeli: ortak kod koordinasyonu | 4 ekibe yayılıyor | Ekibin tamamına biniyor |
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.
| Zaman | Ne oldu | Maliyet |
|---|---|---|
| Çarşamba 10:15 | common’da enum değeri eklendi, PR açıldı | 20 dk |
| Çarşamba 11:00 | Review, merge, 1.14.0 tag, paket yayınlandı | 45 dk |
| Çarşamba 11:30–17:00 | 8 serviste bump PR (1.13 → 1.14). İkisinde derleme hatası: eski bir switch yeni değeri tanımıyor | 8 PR, 2 düzeltme |
| Perşembe | 8 CI (biri flaky, iki tekrar), 8 review, 5 deploy | Bir kişinin günü |
| Cuma 16:30 | Kalan 3 deploy. Raporlama servisi hâlâ 1.13’te: o hafta kimse dokunmadı, bump unutuldu | 7/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.
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.
- Tek repo:
services/altında 8 servis,libs/altında ortak kod. commonpaket 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 cloneile her şeyi görür.
- 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.
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.
# .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ı.
<!-- services/odeme/Odeme.csproj -->
<PackageReference Include="Company.Common" Version="1.13.0" />
<!-- 8 serviste 8 farkli satir; hangisi guncel, kimse bilmiyor -->
<!-- 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:
# 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ı.
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.
Geçiş sırası
- Boş monorepoda CI’ı kur. Path filtresi, servis başına workflow, bağımsız deploy. Tek servisle dene.
- Ortak kütüphaneleri taşı. En çok acı orada, en hızlı kazanım orada.
- Servisleri en az bağımlıdan başlayarak taşı. Her taşımada paket referansı → proje referansı. Servis başına bir PR.
- Co-change analizi çalıştır. Birleştirme adaylarını listele; birleştirme ayrı iş.
- Eski repoları arşivle. Silme.
- 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
- 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-repoile mi taşındı?git log --followeski 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.