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ç, her biri için CI bekle, review al, deploy et. Yedincisi Cuma 16:30’da canlıdaydı. Sekizinci PR hiç açılmadı; unutuldu. 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ı. Bu Conway yasasının repo hâli.
- Ortak kütüphanede 20 dakikalık iş, 3 günlük teslim. 7 bump PR, 7 CI, 7 review, 7 deploy; sekizinci servis unutuldu. Tek olay değil:
commonyılda 19 sürüm alıyor, 4 turda en az bir servis geride kaldı. Monorepoda aynı iş tek PR, tek review, tek commit. - Monorepo ≠ monolit. Tek repo, ama her servis ayrı deploy edilir; CI tetikleyicisi ile deploy tetikleyicisi ayrı. 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; ikinci günde pipeline 31 dakikadır. Path filtresi de bedava değil: elle tutulan bir bağımlılık grafiği ve required check’le birleşince PR’ı sonsuza kadar kilitler.
- Geçmişi taşı, kopyalama.
git filter-repoile, tag’leri yeniden adlandırarak; yoksa iki yıllıkgit blamegider, 8 repodakiv1.0.0çakışır. - 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; 12 aylık co-change verisi hangi ikisinin tek servis olduğunu söyledi.
Repo sınırı neyi çözer?
Projeye üç yıl önce 4 ekip, 14 kişiyle başladık. Ödeme ekibi odeme ve
bildirim servislerini, hesap ekibi hesap, limit ve
mutabakat’ı, raporlama ekibi raporlama ve yonetim’i,
entegrasyon ekibi entegrasyon’u yazıyordu. 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. Conway yasası “sistem, onu üreten organizasyonun iletişim yapısını kopyalar” der; 9 repo, 4 ekibin iletişim yapısının kopyasıydı. Organizasyon değişti, kopya kaldı.
| 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 | Yine 4 dk; ama ortak kod değişince 8 repoda 8 PR’ı aynı kişi açıp izliyor. Makineler paralel çalışır, insan çalışmaz |
| 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ü; 11 Şubat 2026, Çarşamba. Yeni bir limit kuralı için
ErrorCode enum’una tek değer eklenecek: LimitExceeded.
Değişiklik common’da; tüketen 8 servis var, o sabah sekizi de
1.13’te.
| 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 | 7 serviste bump PR (1.13 → 1.14). İkisinde derleme hatası: bir switch expression yeni değeri kapsamıyor, TreatWarningsAsErrors açık olduğu için CS8509 uyarısı bizde hata | 7 PR, 2 düzeltme |
| Perşembe | 7 CI; biri flaky çıktı, iki kez yeniden koşuldu. 7 review, 5 deploy | 9 CI koşusu, bir kişinin günü |
| Cuma 16:30 | Kalan 2 deploy. Sekizinci PR hiç açılmadı: raporlama 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ı tablo 7 servis 1.14, raporlama
1.13. Tek satır fark; ama o satırı iki hafta sonra kimse hatırlamayacaktı ve
“hangi servis hangi common’da?” sorusunun cevabı yine bir
tabloya bakmak olacaktı.
Bu tek olay olsaydı yazı olmazdı. Geçişten önceki 12 ayda common
19 sürüm aldı (minor + patch). 19 × 8 = 152 bump PR açılmalıydı;
açılan 131. Aradaki 21 PR ya unutuldu ya “sonraki turda” dendi; 4 turda en az bir
servis geride kaldı, ikisinde bunu canlıda bir hatayla fark ettik. Yılda 130 PR’ın işi
yalnızca bir versiyon numarasını değiştirmek.
Ucuz alternatif: bump’ı robota vermek
Karar toplantısından önce ilk itiraz kendi içimizden geldi: “Renovate kur, bump PR’larını robot açsın, CI yeşilse auto-merge olsun; repo yapısına dokunmadan biter.” Doğru bir itiraz. Neyi çözdüğünü ve çözmediğini yan yana yazdık:
- 7 PR’ı elle açmak: sıfıra iner.
- Unutulan servis: robot unutmaz, paket çıkınca dakikalar içinde 8 PR açar.
- Versiyon matrisinin dağınıklığı: geride kalan servis en fazla bir CI süresi geride kalır.
- 7 CI, 7 deploy: aynen durur.
- 7 review: auto-merge’e güvenirsen review yok; iki derleme hatası çıkan tur bize güvenmemeyi öğretti.
- Asıl olan: ortak koddaki değişiklik ile onu kullanan servisteki değişiklik yine iki PR, iki repo, iki zaman. Bir interface’i değiştirmek için önce eski + yeni imzayı ekle, sürüm bas, 8 servisi geçir, eskiyi kaldır, tekrar sürüm bas. Expand/contract, 3 kişilik ekipte her küçük refactor için.
Hesap şu: common yılda iki kez değişseydi Renovate yeter, taşımazdık. Yılda 19
kez değişiyor ve o değişikliklerin çoğu “bir alan ekle, kullananı güncelle”
biçiminde; yani iki repoya bölünmüş tek iş. Robot PR açmayı alır, bölünmüşlüğü almaz.
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 kaynak seviyesinde biter: her servis her commit’te aynıcommon’la derlenir.- 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.
- Aynı PR’da değişen 8 servis yine ayrı ayrı, sırayla deploy edilir. Deploy penceresinde iki
commonsürümü aynı anda canlıdır; geriye dönük uyumluluk zorunluluğu aynen durur. - Servisler çalışma zamanında birbirini nasıl çağırıyorsa öyle çağırmaya devam eder.
- “Hepsini birden deploy et” diye bir buton yok. Olmayacak.
Sağdaki listenin ikinci maddesi enum örneğinde şu demek: LimitExceeded’ı
üreten limit ile onu işleyen hesap ve bildirim, deploy
sırasında birkaç dakika farklı sürümde çalışır; hesap, tanımadığı bir hata
kodunu Unknown’a düşürmek zorundadır. Bu zorunluluk poly repo’da da
vardı, Çarşamba ile Cuma arasında iki gün boyunca; sadece kimse yazmamıştı.
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: beş 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.
Bazel ya da dotnet-affected gerekmedi; GitHub Actions’ın paths
filtresi yetti. İkinci kural: CI tetikleyicisi ile deploy tetikleyicisi ayrı dosyada,
ayrı koşulla.
# .github/workflows/odeme-ci.yml
name: odeme-ci
on:
push:
branches: [main]
paths:
- "services/odeme/**"
- "libs/common/**" # ortak kod degisince bu servis de derlenir, test edilir
- ".github/workflows/odeme-ci.yml"
pull_request: # dikkat: burada "paths" yok; sebebi 2. tuzakta
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1 # 470 MB gecmis yerine 4 sn'lik checkout
- uses: dorny/paths-filter@v3
id: chg
with:
filters: |
odeme:
- "services/odeme/**"
- "libs/common/**"
- if: steps.chg.outputs.odeme == 'true'
run: dotnet test services/odeme # sadece bu servis; 4 dk, eskisi gibi
# degismediyse job 20 sn'de yesil biter
# .github/workflows/odeme-deploy.yml
name: odeme-deploy
on:
push:
branches: [main]
paths:
- "services/odeme/**" # libs/common YOK: ortak kod tek basina deploy tetiklemez
workflow_dispatch: # ortak koddaki duzeltme hemen cikmali ise elle; 6 ayda 3 kez
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 1 }
- run: docker build -t registry/odeme:${{ github.sha }} services/odeme
- run: ./deploy.sh odeme ${{ github.sha }} # image etiketi = commit; geri alma bu etiketle
libs/common değişince 8 CI çalışır, sıfır deploy tetiklenir. Ortak koddaki
değişikliğe ihtiyacı olan servis zaten aynı PR’da değişmiştir ve kendi deploy’u
çalışır; ihtiyacı olmayan servis yeni common’ı bir sonraki deploy’unda
alır. “Hepsini birden deploy et” butonu olmamasının anlamı bu.
2. Path filtresi elle tutulan bir bağımlılık grafiğidir
Yukarıdaki paths listesi bir bağımlılık grafiğinin en basit hâli ve iki yönde
bozulur. Kolay olanı: birisi filtreyi fazla genişletir, CI yine 30 dakikaya çıkar; bunu p95
görür. Tehlikeli olanı ters yön: bildirim servisi libs/sablon’a
bağımlılık ekler, workflow’undaki paths listesini güncellemez. O günden
sonra libs/sablon değişince bildirim derlenmez, test edilmez ve kimse
fark etmez. Grafik csproj’da bir yerde, YAML’da başka bir yerde yazılıdır ve
ikisi kendiliğinden senkron kalmaz. Bizim çözümümüz 9 satırlık bir bekçi:
for wf in .github/workflows/*-ci.yml; do
svc=$(basename "$wf" -ci.yml)
grep -oh 'libs/[a-z]*' services/$svc/*.csproj | sort -u | while read -r lib; do
grep -q "$lib/\*\*" "$wf" || echo "EKSIK: $svc -> $lib ($wf)"
done
done
# bir satir bile yazarsa exit 1; PR kirmizi
İkinci tuzak GitHub’a özgü ve monorepo’ların en klasik hatası: workflow’u
branch protection’da required check yaparsın, sonra pull_request
tetikleyicisine paths koyarsın. Yalnızca raporlama’ya dokunan
bir PR’da odeme-ci hiç başlamaz; başlamayan check “skipped”
bile demez, hiç rapor vermez; PR sonsuza kadar “waiting for status” kalır. Bize
pilot serviste, ilk gün oldu. İki çıkış var: aynı name’e sahip ikinci bir workflow’u
paths-ignore ile ters koşula bağlayıp içine boş bir job koymak (GitHub’ın
kendi dokümanındaki yol) ya da bizim yaptığımız: PR tarafında paths’i
kaldırıp filtreyi job’un içine almak. Check her PR’da rapor verir; işi yalnızca
gerektiğinde yapar. Merge queue kullanıyorsan aynı sorun orada da var; ayarını orada da kontrol
et.
Servis sayısı büyüyünce bu listeler bir kişinin kafasında tutulamaz olur ve o gün
dotnet-affected ya da Bazel konuşulur. Sınır kaç servis, bilmiyoruz; bizde 8
servis, 2 lib ve 10 satırlık liste var, 3 kişi ezbere biliyor.
3. 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, PR’ın içinde, Çarşamba 10:35’te ortaya
çıkardı. Bir ayrıntı: onları yakalayan derleyici değil, TreatWarningsAsErrors.
Enum’a değer eklemek C#’ta derleme hatası vermez; switch
expression’da eksik dal CS8509 uyarısıdır. O ayar kapalıysa iki servis sessizce
derlenir ve LimitExceeded çalışma zamanında default dalına düşer.
Unutulan raporlama’ya gelince: unutulacak adım yok; aynı PR’da yeni
common’la derlenir, CI’ı çalışır.
Kaynak referansının bedeli de var; dördünü yazıyorum:
- Eski sürümde kalma hakkı gider. Bir servis “bu değişikliği şimdi alamam” diyemez;
common’daki her değişiklik ya geriye uyumludur ya da 8 servisi aynı PR’da geçirir. Bu bir bedel; bizim için aynı zamanda amaç. - Transitive paket sürümleri hizalanmak zorunda.
commonNewtonsoft 13’e geçince 8 servis geçer. BunuDirectory.Packages.propsile merkezi yönetiyoruz; sürüm tek dosyada. - Target framework tek.
yonetimhâlânet6.0’daydı;net8.0’a taşımak geçişin ön şartı oldu, 2 gün. - Build süresi. Her servisin build’i
common’ı da derler: servis başına +25 saniye. Kabul.
Kuralın istisnası: common’ı repo dışından tüketen varsa (ajans, mobil,
başka bir ekip) paket basmaya devam edersin; o zaman kütüphanenin iki müşterisi vardır,
içeridekiler kaynaktan, dışarıdakiler paketten. Bizde dış tüketici yok. Olsaydı istisna oydu
ve versiyon matrisi o müşteri için sürerdi.
4. 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ş, yollar
yeniden yazılarak taşınır:
# 1) eski repoyu taze klonla (filter-repo, uzerinde calisilmis klonu reddeder)
git clone git@github.com:company/odeme-service.git /tmp/odeme
cd /tmp/odeme
# 2) butun gecmiste yollari services/odeme/ altina tasi;
# tag'leri servis adiyla onekle: v1.4.2 -> odeme/v1.4.2
# (8 repoda da v1.0.0 var; oneklemezsen merge'de cakisir)
git filter-repo --to-subdirectory-filter services/odeme --tag-rename :odeme/
# 3) monorepoya cek; iki gecmisin ortak atasi yok, izin ver
cd ~/monorepo
git remote add odeme /tmp/odeme
git fetch odeme --tags
git merge --allow-unrelated-histories odeme/main -m "odeme servisini tasi (gecmisiyle)"
git remote remove odeme
# kontrol: eski commit'ler yeni yolda gorunuyor mu? (yollar yeniden yazildi, --follow gerekmez)
git log --oneline -- services/odeme/Odeme.csproj | tail -3
Servis başına 15 dakika komut; devir listesi daha uzun sürüyor. Eski repoyu arşivlemeden önce: açık PR’lar (bizde toplam 6; merge ya da kapat, yeni repoda yeniden aç), issue’lar (GitHub transfer), webhook’lar (3 Slack, 1 deploy), deploy credential’ları (yeni repo secrets’ına), README badge’leri, wiki ve dış dokümanlardaki linkler. Sonra repo archived; README’nin ilk satırı “Taşındı: monorepo/services/odeme”. Silinmez; bir link, bir eski doküman hâlâ oraya bakıyor olabilir.
Boyut: 9 reponun toplamı 470 MB’tı, monorepo da 470 MB; sürpriz yok. Sürpriz olan
CI’daydı: tam klon 55 saniye ve 8 workflow bunu 8 kez yapıyordu. fetch-depth: 1
ile checkout 4 saniye. Sparse checkout’a gerek kalmadı. LFS kullanmıyoruz; kullanıyor
olsaydın filter-repo LFS nesnelerini taşımaz, önce onları ayrıca planlaman
gerekir.
5. 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; GitHub’da dizin bazlı okuma izni yok.
CODEOWNERS, branch protection’da “require review from Code
Owners” açıkken review’ı sınırlar; okumayı hiçbir ayarla sınırlamaz. Bugün 3 kişiyiz,
sorun değil. Yarın dışarıdan biri girerse iki seçenek var, ikisini de not olarak yazdık:
ajansın çalışacağı servisi git subtree split ile ayrı bir repoya çıkarıp iki yönlü
senkron tutmak; ya da o servisi monorepoya hiç almamak. İlk aday entegrasyon:
neredeyse hiç kimseyle birlikte değişmiyor (aşağıda rakamı var). Çözülmesi gereken gün bellidir:
ilk dış hesap açıldığı gün.
Tek repoda sürüm, geri alma, hotfix
“Bir servisin hatası diğerinin deploy’unu geri almaz” cümlesi, mekanizması yazılmadan havada kalır. Bizdeki mekanizma üç kural:
- Tag servis başına, önekle.
odeme/v1.4.2,hesap/v2.1.0. Sürüm numaraları eski repodan kaldığı yerden devam eder. Release notu:git log odeme/v1.4.1..odeme/v1.4.2 -- services/odeme libs/; yalnızca o servisi ilgilendiren commit’ler. - Geri alma git seviyesinde değil, image seviyesinde. Her deploy commit SHA’sıyla etiketli bir image; geri almak önceki image’ı tekrar deploy etmek. Git’e dokunulmaz, diğer 7 servis etkilenmez.
common’daki bir hatayı revert edersen 8 CI çalışır, ama yine sıfır deploy tetiklenir; hangi servisin yeni build’e ihtiyacı varsa onu çıkarırsın. - Hotfix main’e. Düzeltme main’e girer, path filtresi yalnızca o servisin deploy’unu tetikler. Release branch yok; 6 ayda cherry-pick sayısı sıfır. Şartı: main her an deploy edilebilir olmalı, o servise ait yarım iş feature flag arkasında durmalı. Bu şartı poly repo’da da koymuştuk; monorepo onu zorunlu kıldı.
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ı. Komutun iki tuzağı var, ikisine de düştük:
- “Aynı commit” ölçütü taşınmış geçmişte çalışmaz.
filter-repoile gelen eski commit’lerin her biri tek servis dizinine dokunur;merge --allow-unrelated-historiescommit’leri birleştirmez, aynı grafiğe koyar. Geçiş öncesi için cevap tanım gereği sıfırdır; ilk denememiz bunu verdi. Doğru ölçüt zaman yakınlığı:filter-repotarihleri korur, aynı gün değişen servis çiftlerini saymak taşınmış geçmişte de çalışır. - Mekanik commit’ler her çifti şişirir. Toplu format,
.editorconfig, global paket yükseltmesi: tek commit 8 dizine dokunur, 28 çiftin hepsine bir puan yazar. Bir günde 4’ten fazla servis dizinine dokunulduysa o günü saymıyoruz. Poly dönemin bump commit’leri de aynı cinsten:commonçıkınca aynı gün 3 servise giren bump PR’ı kuplaj değil; yalnızca.csprojya da lock dosyası değişen commit’leri saymıyoruz.
cochange() { # $1 = baslangic, $2 = bitis
git log --since="$1" --until="$2" --date=short --pretty=format:'DAY %ad' --name-only \
| awk '
# %ad = author date: rebase/cherry-pick ile tasinmis gecmiste committer tarihi goc gunune coker,
# author tarihi kalir. (--since/--until committer tarihine bakar; pencere sinirinda 1-2 gun kayabilir.)
/^DAY / { day = $2; next }
/^services\// && !/\.csproj$/ && !/packages\.lock\.json$/ {
# sadece csproj/lock degisen commit = bump, kuplaj degil; kaynak dosya sart
split($0, p, "/"); k = day SUBSEP p[2]
if (!(k in seen)) { seen[k] = 1; list[day] = list[day] " " p[2] } }
END {
for (day in list) {
n = split(list[day], s, " ")
if (n > 4) { skipped++; continue } # toplu format / global yukseltme gunu: atla
for (i = 1; i <= n; i++)
for (j = i + 1; j <= n; j++) {
a = s[i]; b = s[j]
if (a > b) { t = a; a = b; b = t } # gun icindeki sira rastgele; cifti alfabetik sabitle
pair[a " + " b]++
}
}
for (k in pair) print pair[k], k
print skipped + 0, "(atlanan gun)"
}
' | sort -rn | head -5
}
cochange 2025-09-20 2026-03-20 # poly repo donemi, 6 ay
cochange 2026-03-20 2026-09-18 # monorepo donemi, 6 ay
# cikti (bizde): poly 6 ay mono 6 ay
# hesap + limit 29 38
# hesap + odeme 14 24
# bildirim + odeme 6 9
# hesap + raporlama 1 3
# (atlanan gun) 4 5
# entegrasyon'un girdigi en yuksek cift: 1 / 2 (tesadufen ayni gun)
İki yarı aynı şeyi ölçmüyor, bunu kabul ederek okuyoruz: poly dönemde iki servisi birlikte
değiştiren iş iki repoda iki commit ve bump gecikmesiyle çoğu zaman iki ayrı güne düşer, düşük
sayar; mono dönemde tek PR, tek gün, yüksek sayar. O yüzden iki yarıyı toplamıyoruz.
Güvendiğimiz şey rakam değil, sıra: iki dönemde de aynı dört çift, aynı sırada.
hesap ile limit poly yarıda 29, mono yarıda 38 gün birlikte değişmiş;
bu iki servis değil, ağ üstünden konuşan tek servis. Birleştirme adayı bir numara.
entegrasyon neredeyse hiç kimseyle birlikte değişmemiş, en yüksek çifti 2 gün;
ayrı kalır, dokunulmaz. Birleştirme, geçişle aynı PR’da yapılmaz; önce taşı, sonra ölç,
sonra birleştir. hesap + limit birleştirmesi Ekim’de; ayrı yazı.
Geçiş sırası
Karar Şubat sonunda; geçiş 2–20 Mart 2026, üç hafta, servis başına bir PR. Sıra:
- Boş monorepoda iskelet CI’ı yaz. Servis başına CI ve deploy workflow şablonu, path filtresi. Henüz üstünde denenecek servis yok; şablon.
- Ön şartları kapat. Target framework hizası,
Directory.Packages.props. Bizde 2 gün. - Ortak kütüphaneleri taşı. En çok acı orada, en hızlı kazanım orada; servisler proje referansına ancak bundan sonra geçebilir.
- Pilot: en az bağımlı tek servisi taşı. Paket referansı → proje referansı, tag’ler öneklenmiş, devir listesi kapanmış. İskelet CI ilk kez gerçek bir servisle koşar; required check tuzağı bize burada çıktı.
- Kalan servisleri aynı yolla taşı. Servis başına bir PR.
- Devri doğrula, sonra arşivle. Eski repoda webhook, deploy credential’ı, açık PR ve dış link kalmadığını kontrol et; bir deploy’u yeni repodan uçtan uca çalıştır. Arşiv geri alınabilir, Pazartesi sabahı fark edilen kopuk deploy zinciri değil. Sonra arşivle; silme.
- Co-change analizi çalıştır. Gün bazlı ölçüt taşınmış geçmişte de çalışır; beklemek gerekmez. Birleştirme adaylarını listele; birleştirme ayrı iş.
- Hepsini bir hafta sonunda taşımak. Pazartesi hiçbir CI çalışmaz.
- “CI’ı sonra düzeltiriz.” Sonra, 31 dakikalık pipeline demek.
- Required check’e
pull_request.pathskoymak. İlk ilgisiz PR sonsuza kadar bekler. - Paket basmaya devam etmek.
- Tag’leri öneklemeden merge etmek. 8 tane
v1.0.0. - Servis birleştirmeyi taşımayla aynı PR’da yapmak. İki değişiklik, tek geri alma noktası.
- Eski repoları silmek.
Ne izlemeli? Ve 6 ay sonra ne oldu?
Yazı boyunca ölçüm dedim; ölçmeden bitirmek olmaz. 20 Mart’tan 18 Eylül’e, 6 ay:
- Ortak değişiklik lead time.
libs/common’da PR açılmasından, o değişikliğe ihtiyacı olan son servisin deploy’una. Önce 3 gün; sonra 11 PR’da medyan 5 saat, en uzunu 1 gün (review bekledi). Unutulan servis: 0, çünkü unutulacak adım yok. - PR başına dokunulan servis dizini. Sürekli 8/8 ise ortak kütüphane fazla şey biliyor; sınırı düzelt. Bizde 11
commonPR’ının 2’si 8/8’e dokundu (ikisi de log formatı), geri kalanı 1–3 servis. - Servis başına CI süresi, p95. Path filtresi çalışıyorsa 4 dakikada kalır. Bizde ilk hafta 4 dk 40 sn (klon büyüdü),
fetch-depth: 1sonrası 4 dk 10 sn. 10’u geçerse birisi filtreyi genişletmiştir; 4’ün altına düşen bir servis varsa daraltmıştır, o da kötü. - Haftalık deploy sayısı. Düşüyorsa monorepo korkutuyor demektir. Bizde geçiş öncesi ortalama 9, sonra 14; artışın çoğu
commondeğişikliğinin aynı gün çıkmasından. - Versiyon matrisi. Sıfır satır olmalı. Bir satır bile varsa biri paket basıyor. Bizde sıfır.
Kontrol listesi
- Repo sınırı bugün hangi ekibi hangi ekipten koruyor? Cevap “kimseyi” ise geç.
- Ortak kütüphane yılda kaç sürüm alıyor? Son bir yılda kaç bump PR açıldı, kaçı açılmadı?
- Renovate/Dependabot ile auto-merge yeter miydi? Hangi işi çözmezdi?
- Her servisin kendi CI ve deploy workflow’u var mı?
libs/değişince hangi CI’lar çalışıyor, kaç deploy tetikleniyor? - Required check’lerde
pull_request.pathsvar mı? İlgisiz bir PR merge olabiliyor mu? - csproj’daki lib referansları ile workflow’daki
pathslistesi eşleşiyor mu? Bunu kim kontrol ediyor? - Ortak kod paket olarak mı, kaynak olarak mı referanslanıyor? Dışarıdan tüketen var mı?
- Geçmiş
filter-repoile mi taşındı? Tag’ler öneklendi mi?git logeski commit’leri görüyor mu? - Eski repodan neler devredildi: açık PR, issue, webhook, secret, badge, link?
- Tek servisi geri almak nasıl yapılıyor: image mı, git mi? Hotfix nereye giriyor?
- Kaç kişi kaç servis işletiyor? Hangi çiftler aynı gün değişti; poly ve mono dönemi ayrı baktın mı? Gürültü günlerini ve bump commit’lerini eledin mi?
- Dışarıdan biri repoya girerse ne görür? O gün için hangi seçenek yazılı?
- Eski repolar arşivlendi mi, silindi mi?
Sonuç
Üç yıl önce 9 repo doğru karardı. Bugün doğru bir kararı geri almıyoruz; sebebi ortadan kalktığı için sınırı kaldırıyoruz. 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. Yılda 19 kez.
Monorepo bu bedeli kaldırdı: 3 gün, 5 saat oldu; unutulan servis sıfır. 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 Conway’in cümlesinin repo hâli: 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.