Yazılım geliştirme yaparken, projenin büyüklüğü ve ekibin dinamikleri ne olursa olsun, bir noktada bağımlılık yönetimiyle ilgili temel bir karar vermek gerekiyor: Kod tabanını tek bir büyük depoda (monorepo) mı yoksa her bir bileşeni ayrı bir depoda (polyrepo) mı tutacağız? Bu, basit bir klasör yapısı seçimi gibi görünse de, aslında geliştirme hızından deploy güvenilirliğine, hatta ekip içi iletişime kadar birçok alanı etkileyen stratejik bir karar.
Yirmi yıldır sistemler ve yazılımlarla uğraşırken, hem çok parçalı polyrepo yapılarında kaybolduğum, hem de monoreponun getirdiği karmaşıklıklarla boğuştuğum zamanlar oldu. Her iki yaklaşımın da kendine göre avantajları ve dezavantajları var. Bu yazıda, kendi deneyimlerimden yola çıkarak bu iki yaklaşımı karşılaştıracak ve hangi durumlarda neyi tercih ettiğimi, nedenleriyle birlikte anlatacağım.
Polyrepo Yaklaşımı: Parçalı Yönetimin Zorlukları
Polyrepo yaklaşımı, adından da anlaşılacağı gibi, her bir servisin, uygulamanın veya kütüphanenin kendine ait ayrı bir kod deposunda tutulması anlamına geliyor. Bu, özellikle mikroservis mimarileriyle birlikte popülerleşen, “her şey kendi başına yaşasın” felsefesini destekleyen bir yapı.
Bu yaklaşımın ilk bakışta getirdiği bazı bariz avantajlar var. Her bir depo, kendi başına bağımsız bir yaşam döngüsüne sahip olabiliyor. Kendi CI/CD pipeline’ı, kendi versiyonlama stratejisi ve kendine özgü bağımlılıkları olabilir. Bu durum, özellikle farklı ekiplerin farklı ürünler üzerinde çalıştığı büyük organizasyonlarda, ekiplere daha fazla özerklik sağlıyor. Her ekip kendi temposunda ilerleyebilir, kendi teknoloji stack’ini seçebilir ve diğer projelerden bağımsız olarak deploy yapabilir. Ancak bu özerkliğin bir bedeli oluyor.
Ortak bir kütüphaneyi (örneğin bir entegrasyon modülünü) güncellemek gerektiğinde, bu modülü kullanan çok sayıda mikroservisi tek tek güncellemek ve deploy etmek zorunda kalırsınız. Her bir servis için ayrı bir pull request oluşturmak, ayrı testleri çalıştırmak ve ayrı deploy süreçlerini yönetmek, testlerle birlikte ciddi zaman alıyor. Bu durum, küçük bir değişiklik için bile gereksiz bir operasyonel yük oluşturuyor. Ayrıca, farklı depolarda çalışan CI/CD pipeline’larını senkronize etmek ve yönetmek, zamanla karmaşık bir yapıya dönüşebiliyor. Her depo için ayrı bir Jenkinsfile veya GitHub Actions konfigürasyonu tutmak, zamanla tutarsızlıkları da beraberinde getirebiliyor.
# Örnek bir polyrepo'da bağımlılık güncellemeleri
# Microservice A
cd microservice-a
npm update [email protected]
npm test
git commit -m "Update common-library to 1.2.0"
git push
# Microservice B (aynı kütüphaneyi kullanıyor ama farklı bir ekip yönetiyor)
cd microservice-b
# Burada 1.2.0 yerine 1.1.0'da kalmış olabilirler, veya güncellediklerinde farklı bir hata çıkabilir.
npm update [email protected]
npm test
# Testler başarısız olursa, debug süreci başlıyor...
Bu tarz senaryolarda, özellikle shared kütüphanelerin sık güncellendiği veya tüm uygulamaların aynı anda uyumlu olması gereken durumlarda polyrepo yaklaşımı beni çok zorladı. Bir değişiklik birden fazla depoyu etkilediğinde, bu değişikliklerin koordinasyonu ve atomik olarak deploy edilmesi neredeyse imkansız hale geliyor.
Monorepo Yaklaşımı: Bütüncül Bir Bakış
Monorepo, tüm projelerin, kütüphanelerin ve servislerin tek bir versiyon kontrol deposunda tutulduğu yaklaşımdır. Bu, ilk duyulduğunda “tüm yumurtaları tek sepete koymak” gibi gelse de, doğru araçlar ve stratejilerle yönetildiğinde ciddi avantajlar sağlayabiliyor.
Benim için monorepo’nun en büyük artısı, bağımlılık yönetiminin basitleşmesi. Tüm kod tek bir yerde olduğu için, shared kütüphanelerin veya iç modüllerin tek bir versiyonunu kullanmak doğal hale geliyor. Bu, “dependency hell” senaryolarının önüne geçiyor. Bir üretim ERP’sinde, üretim planlama, operatör ekranları ve raporlama modülleri gibi kritik birimlerin tümü aynı kod tabanında olduğunda, ortak bir güvenlik modülünü güncellediğimde tüm bağımlı uygulamaların aynı anda tek bir PR ile güncellenmesi, denetim ve dağıtım süreçlerini inanılmaz kolaylaştırdı.
Kendi yan ürünümün backend’inde, tüm servislerimi tek bir monorepo altında topladığımda, bağımlılık çakışmaları belirgin şekilde azaldı diyebilirim. Ayrıca, farklı projeler arasında kod paylaşımı ve yeniden kullanım çok daha kolay hale geliyor. Bir projeden diğerine kod kopyalamak yerine, doğrudan import edebilir veya ortak bir kütüphane oluşturup bunu tüm projelerde kullanabilirim. Bu, kod kalitesini artırır ve tekrar eden işleri azaltır.
Ancak monorepo’nun da kendi zorlukları var. Repo boyutu zamanla çok büyüyebilir. Bir ERP projesinde monorepo boyutu epeyce şiştiğinde, git clone süreleri uzayabiliyordu. Bu da yeni ekip üyelerinin onboarding süresini uzatıyor ve hatta günlük geliştirme akışını yavaşlatabiliyor. Ayrıca, doğru tooling olmadan monorepo yönetimi bir kabusa dönüşebilir. Her commit’te tüm monoreponun testlerini çalıştırmak, CI pipeline sürelerini ciddi şekilde uzatabiliyordu. Bu durum, hızlı geri bildirim almayı imkansız hale getiriyordu.
# Örnek pnpm-workspace.yaml dosyası (monorepo tooling örneği)
packages:
- 'apps/*' # Uygulamalarım: web, admin, api
- 'packages/*' # Ortak kütüphanelerim: ui-kit, common-utils, types
Bu yapı sayesinde pnpm install komutu, tüm projelerimin bağımlılıklarını tek seferde ve oldukça hızlı bir şekilde yönetebiliyor. İlk başlarda CI sürecinde yaşadığım yavaşlıkları, Nx veya Turborepo gibi araçların affected komutları sayesinde aşmayı başardım. Sadece değişen projelere ait testleri ve build’leri çalıştırdığımda, CI döngüleri belirgin şekilde hızlandı. Bu, monorepo’nun potansiyel dezavantajlarını doğru araçlarla nasıl avantaja çevirebileceğimi gösterdi.
Bağımlılık Yönetiminde Pratik Çözümler ve Araçlar
Her iki yaklaşımın da kendine özgü zorlukları olduğunu gördüğüm için, bağımlılık yönetiminde kullandığım pratik çözümler ve araçlar büyük önem taşıyor. Amacım, karmaşıklığı azaltırken güvenilirliği artırmak.
Polyrepo senaryolarında, versiyon çakışmalarını en aza indirmek için Semantic Versioning (SemVer) kurallarına sıkı sıkıya bağlı kalıyorum. Ayrıca, bağımlılıkları sabitlemek (pinning) için araçlar kullanıyorum. Örneğin, Python projelerinde pip-compile ile requirements.txt dosyalarını otomatik olarak oluşturup, tüm transitif bağımlılıkları da kesin versiyonlarıyla belirliyorum. Birden çok mikroservisin aynı bağımlılıkları paylaştığı durumlarda bu yöntem, production ortamındaki versiyon sapmalarından kaynaklanan sorunları büyük ölçüde engelliyor. JavaScript ekosisteminde ise package-lock.json veya yarn.lock dosyalarının commit edilmesi bu amaca hizmet ediyor.
# Python projesinde bağımlılıkları sabitleme örneği
# requirements.in dosyam
flask
requests==2.28.1
# pip-compile komutu ile requirements.txt oluşturma
pip-compile requirements.in -o requirements.txt
# Oluşan requirements.txt içeriği (örnek)
#
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile requirements.in
#
flask==2.3.2
itsdangerous<2.2,>=2.1.0
Jinja2<3.2,>=3.0.3
Werkzeug<2.4,>=2.3.3
requests==2.28.1
charset-normalizer<3,>=2
idna<4,>=2.5
urllib3<1.27,>=1.21.1
Monorepo tarafında ise, modern JavaScript ekosistemindeki workspaces özellikleri hayatımı kolaylaştırıyor. npm workspaces, yarn workspaces veya benim favorim olan pnpm workspaces sayesinde, tek bir depo içinde birden fazla proje ve kütüphaneyi yönetebiliyorum. Kendi bir yan ürünümün React frontend ve FastAPI backend’ini içeren monorepoda, pnpm workspaces kullanarak bağımlılıkları tek bir yerden yönetiyorum. pnpm install komutu tüm bağımlılıkları kısa sürede yüklüyor ve disk üzerinde de yerden tasarruf sağlıyor.
Monorepoları daha verimli hale getirmek için Nx, Turborepo veya Bazel gibi özel build sistemleri kullanıyorum. Bu araçlar, sadece değişen kod parçalarını tespit ederek, CI/CD pipeline’larında sadece ilgili testleri ve build’leri çalıştırmamı sağlıyor. Nx’in affected komutlarıyla yalnızca değişen projelere ait testleri ve build’leri çalıştırmak, CI döngülerini belirgin şekilde hızlandırıp uzun pipeline sürelerini kayda değer ölçüde kısaltabiliyor. Bu optimizasyonlar olmadan monorepo’nun faydalarını tam olarak görmek mümkün değil.
Ayrıca, bağımlılıkların otomatik olarak güncellenmesini sağlayan Dependabot gibi servisleri de kullanıyorum. Bu, güvenlik açıklarını kapatmak ve kütüphaneleri güncel tutmak için kritik. Ancak, otomatik güncellemelerin test süreçlerinden geçmesi ve olası breaking change’lere karşı dikkatli olunması gerekiyor. Bu araçlar, manuel bağımlılık yönetimi yükünü önemli ölçüde azaltıyor.
Karar Verirken Göz Önünde Bulundurmam Gerekenler
Monorepo ve polyrepo arasında seçim yaparken, sadece teknik detaylara değil, aynı zamanda organizasyonel ve operasyonel faktörlere de bakmak gerekiyor. Kendi projelerimde ve çalıştığım şirketlerde bu kararları verirken şunları göz önünde bulunduruyorum:
-
Ekip Büyüklüğü ve Yapısı: Küçük, tek bir ekiple çalışan projelerde monorepo’nun getirdiği basitlik ve hız genellikle daha avantajlı oluyor. Herkes aynı kod tabanında olduğu için koordinasyon daha kolay. Ancak, farklı ürünler veya iş alanları üzerinde çalışan büyük, bağımsız ekipler için polyrepo’lar daha fazla özerklik sağlayabilir. Yine de, büyük ekiplerde de monorepo’yu doğru araçlarla (Nx gibi) yönetmek, ortak kütüphanelerin tutarlılığını sağlamak açısından faydalı olabilir.
-
Projenin Büyüklüğü ve Karmaşıklığı: Projenin ne kadar ortak bileşeni var? Bu bileşenler ne sıklıkla değişiyor? Eğer çok sayıda shared kütüphane ve modül varsa ve bunlar sık sık güncelleniyorsa, monorepo’nun atomic commit yeteneği büyük bir avantaj sağlar. Eğer projeler birbirinden tamamen bağımsız ve çok az ortak nokta içeriyorsa, polyrepo daha mantıklı olabilir. Kendi Android spam uygulamamın native kodu ile bir yan ürünümün web tabanlı finansal hesaplayıcısının kodunun aynı repoda olması pek mantıklı değil, çünkü teknoloji stack’leri ve bağımlılıkları tamamen farklı.
-
CI/CD Süreçleri ve Gerekli Performans: Build ve deploy süreleri ne kadar kritik? Monorepo’da her ne kadar akıllı build sistemleri ile optimizasyon yapılabilse de, başlangıçta ve doğru araçlar olmadan CI/CD süreçleri yavaşlayabilir. Bir projemde, build sürelerinin uzaması kabul edilemezdi. Bu yüzden monoreponun getirdiği incremental builds gibi build optimizasyon yetenekleri kritik hale geldi. Polyrepo’da ise her proje için ayrı pipeline’lar kurmak ve bunları yönetmek operasyonel olarak daha fazla efor gerektirebilir.
-
Güvenlik Endişeleri: Tek bir depoda tüm kodun tutulması, teorik olarak tek bir güvenlik ihlalinin tüm projelere erişim sağlaması riskini barındırır. Polyrepo’da ise risk daha dağıtılmıştır. Ancak, iyi bir güvenlik pratiği ve erişim kontrolü ile bu risk monorepo’da da yönetilebilir. Önemli olan, risk toleransını ve uygulanacak güvenlik katmanlarını doğru belirlemektir.
-
Organizasyonel Kültür: Ekipler özerkliği mi, yoksa sıkı koordinasyonu mu tercih ediyor? Bu, genellikle göz ardı edilen ama çok önemli bir faktör. Eğer ekipler kendi bağımsızlıklarına çok düşkünse, monorepo’ya geçiş süreci kültürel olarak zorlayıcı olabilir. Ancak, ortak hedeflere ulaşmak için ekipler arası işbirliğinin teşvik edildiği bir ortamda monorepo daha kolay benimsenebilir.
Bu faktörleri bir araya getirerek, her projenin kendine özgü ihtiyaçlarına göre en uygun yaklaşımı belirlemeye çalışıyorum. Her zaman tek bir doğru cevap olmadığını kendi tecrübelerimle gördüm.
Benim Tercihlerim ve Nedenleri
Monorepo ve polyrepo yaklaşımlarını defalarca denedim ve sonunda kendi adıma bazı net tercihler geliştirdim. Artık çoğu senaryoda monorepo’ya daha yakın duruyorum, özellikle de birbiriyle ilişkili ve ortak kütüphaneler kullanan projeler için.
Neden mi? Çünkü monorepo’nun getirdiği atomik değişiklik yapabilme ve birleşik bağımlılık yönetimi faydaları, modern monorepo araçlarıyla (Nx, Turborepo) aşılabilecek tooling karmaşıklığının çok ötesine geçiyor. Özellikle bir üretim planlama gibi kritik birimlerin olduğu bir ERP projesinde, üretim hattını etkileyebilecek bir bağımlılık değişimini tek bir PR içinde tüm operatör ekranları ve raporlama modülleriyle birlikte test edip deploy edebilmek benim için çok büyük bir avantaj. Bu, olası hataların kapsamını daraltıyor ve rollback süreçlerini basitleştiriyor.
Kendi yan ürünlerimin çoğunda, web frontend, API backend ve ortak kütüphaneleri tek bir repoda tutuyorum. Bu, geliştirme hızımı ve deploy güvenilirliğimi artırdı. Örneğin, kendi siteme yaptığım finansal hesaplayıcılarda, hem frontend hem de backend’in aynı tip tanım dosyalarını (TypeScript types) kullanması, monorepo sayesinde çok daha kolay hale geliyor. Bir tip tanımını değiştirdiğimde, hem frontend hem de backend’in bu değişikliğe uyum sağladığından emin olmak tek bir PR ile mümkün oluyor.
Ancak, her zaman monorepo tercih etmiyorum. Çok farklı teknoloji stack’lerine sahip, birbiriyle neredeyse hiç etkileşmeyen ve farklı ekipler tarafından yönetilen bağımsız ürünler için polyrepo hala mantıklı olabilir. Örneğin, dediğim gibi, Android spam uygulamamın native kodu ile bir yan ürünümün web tabanlı finansal hesaplayıcısının kodu aynı repoda olmamalı. Bu iki proje arasındaki bağımlılık neredeyse sıfır ve farklı platformlarda, farklı dillerde yazılmışlar. Bu durumda monorepo’nun getireceği tek şey gereksiz karmaşıklık olurdu.
Bazen de “hybrid” yaklaşımlar benimsiyorum. Yani, mantıksal olarak bir monorepo gibi davranan ama fiziksel olarak ayrı repoları olan bir yapı. Bu, genellikle çok büyük ve eski sistemlerin modernizasyonu sırasında karşıma çıkıyor. Ortak kütüphaneleri ayrı bir depoda tutarken, uygulamaların kendi depoları olabiliyor. Bu durumda, sıkı bir versiyonlama ve sürüm yönetimi stratejisi uygulamak gerekiyor. Yazılımda Sürüm Yönetimi ve Otomasyon
Sonuç olarak, benim tercihim çoğunlukla monorepo yönünde. Ancak bu tercih, her zaman projenin ve ekibin özel ihtiyaçları, ölçeği ve beklentileri doğrultusunda şekilleniyor. Önemli olan, her iki yaklaşımın da güçlü ve zayıf yönlerini bilmek ve doğru zamanda doğru aracı seçebilmek.
Sonuç
Yazılım projelerinde dependency yönetimi için monorepo ve polyrepo yaklaşımları, her birinin kendine özgü avantaj ve dezavantajlarıyla birlikte karşımıza çıkan temel bir mimari karardır. Bu iki yaklaşımın da farklı senaryolarda nasıl çalıştığını, hangi zorlukları beraberinde getirdiğini ve hangi faydaları sağladığını bizzat deneyimledim.
Polyrepo, küçük, bağımsız ve farklı teknoloji stack’lerine sahip projeler için başlangıçta daha basit görünebilirken, özellikle ortak kütüphanelerin sık güncellendiği ve projeler arası koordinasyonun kritik olduğu durumlarda “dependency hell” ve operasyonel karmaşıklık gibi sorunlara yol açabiliyor. Monorepo ise, doğru araçlar (Nx, Turborepo) ve stratejilerle yönetildiğinde, atomik commit’ler, basitleştirilmiş bağımlılık yönetimi ve kolay refactoring gibi önemli avantajlar sunarak geliştirme hızını ve güvenilirliğini artırabiliyor. CI/CD Pipeline Güvenilirliği
Unutmamak gerekir ki, tek bir “en iyi” çözüm yoktur. Karar verirken ekip büyüklüğü, projenin karmaşıklığı, CI/CD beklentileri, güvenlik endişeleri ve hatta organizasyonel kültür gibi faktörleri dikkate almak gerekiyor. Benim kişisel tercihim genellikle monorepo’dan yana olsa da, her projenin kendi bağlamında değerlendirilmesi gerektiğine inanıyorum. Önemli olan, bu yaklaşımların tüm nüanslarını anlamak ve projenizin ihtiyaçlarına en uygun olanı bilinçli bir şekilde seçmektir.