Yıllar içinde birçok yazılım projesinde, küçük yan ürünlerimden tutun da büyük kurumsal ERP sistemlerine kadar, farklı repo yapıları kullandım. Her projenin başında “monorepo mu polyrepo mu” sorusuyla karşılaştığımda, bu kararın sadece kodun nerede durduğundan ibaret olmadığını, CI/CD süreçlerinden ekip motivasyonuna, hatta şirketin uzun vadeli teknoloji stratejisine kadar çok geniş etkileri olduğunu gördüm. Bu yazıda, bu tercihin CI/CD süreçleri özelindeki kritik 3 sonucunu kendi deneyimlerimle anlatmak istiyorum.
Bu kararı verirken her zaman “benim için neyin daha iyi olacağı” değil, “projenin ve ekibin dinamiklerine neyin daha uygun düşeceği” sorusunu sordum. Çünkü bir proje için ideal olan, başka bir proje için tam bir kabus olabiliyordu. Doğru tercihi yapmak, ileride yaşanacak birçok operasyonel sancıyı daha başlamadan engellemek demekti.
Monorepo ve Polyrepo’ya Benim Bakışım: Bir Tanım Denemesi
Monorepo’yu, tüm projelerimin, servislerimin ve kütüphanelerimin tek bir Git repository’sinde yaşadığı bir yapı olarak tanımlıyorum. Bu, benim kendi Android spam blocker uygulamamın hem backend’ini hem frontend’ini aynı repo’da tuttuğum ilk zamanlardaki gibi bir yaklaşımdı. Her şey tek bir yerdeydi, değişiklikleri takip etmek kolaydı ve bağımlılıkları yönetmek daha az karmaşıktı. Özellikle küçük ekiplerde veya tek başıma çalıştığım projelerde bu bana büyük kolaylık sağlamıştı.
Öte yandan polyrepo ise her servis, kütüphane veya bağımsız uygulama için ayrı bir Git repository’si kullanmak anlamına geliyor. Bir üretim firmasının ERP’sini geliştirirken yüzlerce farklı servis ve modülümüz vardı; bunların her birini ayrı bir repo’da tutmayı tercih ettik. Bu, ekiplerin daha otonom çalışmasını, kendi release döngülerini yönetmesini ve birbirlerinden bağımsız ilerlemesini sağlıyordu. Ancak bu bağımsızlığın getirdiği farklı zorluklar da vardı, özellikle de CI/CD süreçleri söz konusu olduğunda.
Kendi yan ürünlerimden birinde, başlangıçta monorepo benzeri bir yapıyla yola çıktım. Frontend (Vue) ve backend (FastAPI) kodları aynı repo’daydı. Basit bir git push ile her iki tarafın da CI/CD’sinin tetiklenmesi ve her şeyin birlikte deploy edilmesi benim için yeterliydi. Ancak proje büyüdükçe ve farklı modüller eklemeye başladıkça, her commit’te tüm sistemin tekrar build edilmesi ve test edilmesi zaman kaybettirmeye başladı. İşte o noktada, monorepo’nun getirdiği avantajların yanında bazı zorlukları da görmeye başladım.
CI/CD Süreçlerine Etkisi: Build ve Test Optimizasyonu
Monorepo’da en büyük CI/CD zorluğu, her commit’te tüm sistemin baştan sona build edilmesi ve test edilmesiydi. Özellikle büyük projelerde bu, saatler süren pipeline’lar anlamına geliyordu. Bir üretim firmasının ERP’sinde, full bir build ve test döngüsü saatlerce sürebiliyordu. Bu da geliştiricilerin geri bildirim almasını geciktiriyor ve iterasyon hızını düşürüyordu. İlk başta “olur o kadar” dedim, ama bir süre sonra bu durumun sürdürülemez olduğunu anladım.
Çözüm olarak, monorepo araçlarının (Nx, Bazel gibi) “affected” komutlarının mantığını kendi CI/CD pipeline’larıma uygulamaya çalıştım. git diff komutunu kullanarak, sadece değişen kod parçalarını ve bunların bağımlı olduğu diğer modülleri tespit edip, sadece onları build ve test etmeyi hedefledim. Örneğin, git diff --name-only HEAD~1 HEAD ile son commit’teki dosyaları alıp, bir script yardımıyla hangi servislerin etkilendiğini buldum.
# Sadece değişen servisleri tespit eden bir örnek CI/CD adımı
CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD)
AFFECTED_SERVICES=""
if echo "$CHANGED_FILES" | grep -q "services/order/"; then
AFFECTED_SERVICES+="order "
fi
if echo "$CHANGED_FILES" | grep -q "services/inventory/"; then
AFFECTED_SERVICES+="inventory "
fi
# ... diğer servisler için devam eder
if [ -n "$AFFECTED_SERVICES" ]; then
echo "Değişen servisler: $AFFECTED_SERVICES"
# Sadece etkilenen servisleri build et ve test et
for SERVICE in $AFFECTED_SERVICES; do
echo "Building and testing $SERVICE..."
# build_service.sh ve test_service.sh scriptleri
# sadece ilgili servisi isleyecek sekilde yazilmisti.
./scripts/build_service.sh "$SERVICE"
./scripts/test_service.sh "$SERVICE"
done
else
echo "Hiçbir servis etkilenmedi, CI/CD adımı atlanıyor."
fi
Bu yaklaşım, monorepo’nun build sürelerini belirgin biçimde kısaltarak, saatlerce süren döngüyü çok daha makul bir aralığa indirdi. Polyrepo’da ise durum farklıydı. Her repo’nun kendi CI/CD pipeline’ı olduğu için, sadece ilgili repo’daki kod değiştiğinde pipeline tetikleniyordu. Bu da her bir servis için hızlı geri bildirim sağlıyordu. Ancak, bir bağımlılığın değişmesi (örneğin, ortak bir kütüphane) durumunda, bu kütüphaneyi kullanan tüm repoların manuel olarak veya karmaşık bir tetikleme mekanizmasıyla yeniden build edilmesi gerekiyordu. Bu da ayrı bir yönetim yükü getiriyordu. CI/CD pipeline optimizasyonu konusunda daha detaylı bir analiz yapmıştım.
Deployment ve Rollback Karmaşası: Versiyonlama ve Bağımlılıklar
Monorepo’da deployment, genellikle “atomic deploy” veya “independent deploy” stratejileriyle yönetilir. Atomic deploy, tüm sistemin tek bir birim olarak deploy edilmesi anlamına gelir. Bu, özellikle küçük ve orta ölçekli sistemlerde veya mikroservislerin birbirine çok bağımlı olduğu durumlarda işleri basitleştirir. Tüm servisler aynı anda yeni versiyona geçer, bu da uyumluluk sorunlarını azaltır. Ancak, bir servisteki ufak bir değişiklik için bile tüm sistemi yeniden deploy etmek, riskli ve yavaş olabilir. Benim ERP projemde, küçük bir raporlama hatası için tüm sistemi deploy etmek zorunda kalmak gözle görülür bir kesintiye yol açmıştı.
Independent deploy ise monorepo içinde bile her servisin kendi hızında deploy edilmesini sağlar. Bu, yukarıdaki git diff örneğinde olduğu gibi, sadece değişen servislerin deploy edilmesidir. Bu yaklaşım, deploy hızını artırır ve riski azaltır. Ancak, servisler arası versiyon uyumluluğunu yönetmek zorlaşır. Bir servis A’nın yeni versiyonu, eski versiyon B’ye bağımlı olan başka bir servis C’yi kırabilir. Bu, bana API versioning stratejileri konusunda yaşadığım sıkıntıları hatırlatıyor.
Polyrepo’da ise her servis kendi başına deploy edildiği için, versiyonlama ve bağımlılık yönetimi tam bir cehenneme dönüşebiliyordu. Onlarca microservice’in olduğu bir platformda her servisin kendi versiyon numarası ve bağımlılıkları vardı. Bir servisin belirli bir versiyonu, başka servislerin belirli versiyonlarına bağımlıyken, üçüncü bir servisin yeni sürümü o servislerden birinin yalnızca eski bir minor versiyonuyla çalışabiliyordu. Bu bağımlılık matrisini takip etmek, hangi servisin hangi versiyonla uyumlu olduğunu bilmek, deploy ve rollback senaryolarında büyük bir karmaşa yaratıyordu.
Rollback durumlarında da durum benzerdi. Monorepo’da atomic deploy yapıyorsanız, tek bir commit’e geri dönmek nispeten kolaydır. Ancak independent deploy yapıyorsanız, hangi servisin hangi versiyonunu geri alacağınızı doğru tespit etmeniz gerekir. Polyrepo’da ise, bir servisi geri alırken, bu servise bağımlı diğer servislerin de uyumlu olup olmadığını kontrol etmek ve gerekirse onları da geri almak zorunda kalabiliyorsunuz. Bu, özellikle gecenin bir yarısı bir prod hatasıyla uğraşırken istenen son şeydi.
Ekip Yapısı ve Geliştirici Deneyimi: Hızlı Iterasyon mu, Bağımsızlık mı?
Monorepo’nun geliştirici deneyimi üzerindeki en bariz etkisi, kod paylaşımı ve “find-and-replace” gücüydü. Ortak kütüphaneler, UI bileşenleri veya konfigürasyon dosyaları tek bir yerde olduğu için, bir değişiklik yaptığımda bunun tüm sistemi nasıl etkileyeceğini görmek ve gerekli güncellemeleri kolayca yapmak mümkündü. Kendi mobil uygulamamın backend’inde bir API kontratı değiştirdiğimde, frontend kodunu da aynı commit içinde güncelleyebiliyordum. Bu, hızlı iterasyon ve tutarlılık sağlıyordu.
Ancak, monorepo’nun bazı dezavantajları da vardı. Herkesin aynı codebase üzerinde çalışması, merge çakışmalarını artırabiliyordu. Ayrıca, büyük bir monorepo’da kod tabanının tamamına hakim olmak zordu. Bazen alakasız bir modüldeki bir değişiklik, benim hiç beklemediğim bir yerde sorun yaratabiliyordu. Bir veri tabanı şema değişikliğinin alakasız bir raporlama servisini nasıl etkilediğini bulmak günler süren bir uğraş olabiliyordu.
Polyrepo, ekiplere daha fazla otonomi ve bağımsızlık sağlıyordu. Her ekip kendi servislerinin repo’suna sahipti ve kendi hızında ilerleyebiliyordu. Bu, özellikle büyük bir e-ticaret sitesinde gördüğüm gibi, yüzlerce geliştiricinin çalıştığı ve farklı ürün ekiplerinin olduğu ortamlarda çok değerliydi. Her ekip, kendi teknolojisini, CI/CD pipeline’ını ve release döngüsünü seçebiliyordu (tabii belirli sınırlar içinde). Bu, “bizim işimize karışmayın, biz kendi yolumuza bakarız” diyen ekipler için biçilmiş kaftandı.
Ancak bu bağımsızlığın bedeli de vardı. Ortak kod paylaşımı zorlaşıyordu. Bir kütüphane güncellemesi gerektiğinde, bu kütüphaneyi kullanan tüm repolarda ayrı ayrı güncelleme yapılması gerekiyordu. Bu da çok fazla manuel iş yükü ve tutarsızlık potansiyeli yaratıyordu. Ortak bir güvenlik yaması (örneğin, bir kimlik doğrulama kütüphanesindeki zafiyet) geldiğinde, bu kütüphaneyi kullanan tüm repoları tek tek tarayıp güncellemek zorunda kalıyorduk.
Güvenlik ve Uyumluluk: Ortak Zafiyetler ve İzolasyon
Güvenlik perspektifinden baktığımda, monorepo ve polyrepo’nun her ikisinin de kendine göre avantaj ve dezavantajları olduğunu görüyorum. Monorepo’da, ortak bağımlılıkların yönetimi merkezi olduğu için, bir güvenlik açığı (örneğin, ünlü log4j zafiyeti gibi) tespit edildiğinde, etkilenen tüm modülleri tek bir yerde görmek ve yama uygulamak nispeten daha kolaydı. Tek bir PR ile tüm sistemdeki bağımlılık versiyonunu güncelleyebilmek, hızlı aksiyon almak açısından büyük bir avantajdı. log4j krizi patlak verdiğinde, monorepo kullanan bir projede fix’i uygulamak ve deploy etmek kısa sürede halledilebilmişti.
Ancak, monorepo’da bir güvenlik açığının tüm sistemi etkileme potansiyeli de daha yüksektir. Ortak kullanılan bir kütüphanedeki zafiyet, tüm projelere yayılabilir. Ayrıca, farklı güvenlik seviyelerine sahip modüllerin aynı repo’da bulunması, yetkilendirme ve erişim kontrolü açısından karmaşıklık yaratabilir. Örneğin, hassas finansal verilerle ilgilenen bir modül ile halka açık bir pazarlama sayfasının kodunun aynı repo’da olması, repo’ya erişim politikalarını daha sıkı hale getirmeyi gerektirir. Yazılım güvenliği best practices konusunda daha önce değindiğim gibi, bu tür riskleri minimize etmek esastır.
Polyrepo ise daha iyi bir izolasyon sağlıyordu. Her servisin kendi repo’su olduğu için, bir servisteki güvenlik açığı genellikle diğer servisleri doğrudan etkilemiyordu. Bu, “blast radius”ı (bir hatanın veya saldırının potansiyel etkisi) sınırlamaya yardımcı oluyordu. Bir bankanın iç platformunda, kritik finansal servislerle ilgili kodlar, daha az hassas olan iç araçların kodlarından tamamen ayrı repolarda tutuluyordu. Bu da her bir repo için farklı güvenlik politikaları ve erişim kontrolleri uygulamamıza olanak tanıyordu.
Ancak, polyrepo’da güvenlik takibi ve yama uygulaması ciddi bir operasyonel yük oluşturuyordu. Çok sayıda repo’nun her birini ayrı ayrı taramak, güncel olmayan bağımlılıkları tespit etmek ve yamaları uygulamak için merkezi bir sistem kurmak zorundaydım. Ortak bir bağımlılıktaki bir zafiyet ortaya çıktığında, etkilenen repoları elle bulup tek tek güncellemek hem yavaş hem hataya açıktı. Bu durum, otomatikleştirilmiş bir bağımlılık yönetim ve güvenlik tarama sisteminin ne kadar kritik olduğunu bir kez daha gösterdi.
Benim Tercihim ve Çıkarılan Dersler: Duruma Göre Esneklik
Monorepo mu yoksa polyrepo mu sorusuna benim için net bir “her zaman bu” cevabı yok. Her projenin kendi dinamikleri, ekip büyüklüğü, projenin karmaşıklığı ve hatta şirketin kültürü bu kararda etkili oluyor. Ancak yıllar içinde edindiğim tecrübelerle bazı genel prensipler geliştirdim.
Küçük ekiplerle başladığım veya tek başıma geliştirdiğim yan ürünlerde, başlangıçta her zaman monorepo’yu tercih ettim. Hızlı iterasyon, kolay kod paylaşımı ve tek bir CI/CD pipeline’ı yönetme kolaylığı benim için paha biçilmezdi. Kendi finansal hesaplayıcılarımın backend ve frontend’ini aynı repo’da tutmak, yeni bir özellik eklediğimde iki tarafı da eş zamanlı olarak geliştirmemi ve deploy etmemi sağlıyordu. Bu, özellikle startup ruhuyla hızlıca ürün çıkarmak isteyenler için mantıklı bir başlangıç noktası olabilir.
Ancak proje büyüdükçe, ekip sayısı arttıkça ve farklı iş domainlerine ayrılmaya başladıkça, polyrepo’ya geçişin faydalarını görmeye başladım. Özellikle bir üretim firmasının ERP’sinde, üretim planlama, tedarik zinciri, finans gibi tamamen farklı domainlere ait modüllerin kendi bağımsız yaşam döngülerine sahip olması gerektiğinde polyrepo daha uygun hale geliyordu. Her ekibin kendi servislerini bağımsızca geliştirmesi ve deploy etmesi, toplam verimliliği artırıyordu. Tabii ki bu geçişin maliyeti ve zorlukları da vardı; tüm bağımlılıkları ayırmak, yeni CI/CD pipeline’ları kurmak ve versiyonlama stratejisi belirlemek aylar süren bir çalışmaydı.
Çıkarılan en önemli derslerden biri, bu kararın bir kere alınıp asla değiştirilemeyecek bir karar olmadığıydı. Projenin evrimine göre repo yapısını da evrimleştirmek gerekebiliyor. Önemli olan, her iki yapının da avantajlarını ve dezavantajlarını bilmek ve projenin mevcut durumu ile gelecekteki hedeflerini göz önünde bulundurarak en uygun kararı vermektir. Benim deneyimimde, genellikle küçükten başlayıp büyüdükçe parçalara ayırmak, baştan büyük bir polyrepo karmaşasına girmekten daha az sancılı oldu.
Sonuç
Monorepo ve polyrepo seçimi, sadece teknik bir tercih olmaktan öte, CI/CD süreçlerinin verimliliğini, deploy stratejilerinin karmaşıklığını ve ekiplerin çalışma şekillerini derinden etkileyen stratejik bir karardır. Monorepo, başlangıçta hızlı iterasyon ve merkezi yönetim kolaylığı sunarken, ölçek büyüdükçe build süreleri ve bağımlılık yönetimi konusunda zorluklar çıkarabilir. Polyrepo ise ekip otonomisi ve izolasyon sağlarken, bağımlılık matrisini ve güvenlik takibini karmaşık hale getirebilir.
Bu kararı verirken, her zaman projenin mevcut büyüklüğünü, ekip yapısını, gelecekteki ölçeklenme ihtiyaçlarını ve güvenlik gereksinimlerini göz önünde bulundurdum. Her iki yaklaşımın da kendi içinde güzellikleri ve zorlukları var. Önemli olan, bu trade-off’ları iyi anlamak ve projeniz için en pragmatik çözümü bulmaktır. Unutmayın, bu bir “ya hep ya hiç” kararı değil, sürekli gelişen bir mimari tercihi.