Geçenlerde hesapciyiz.com’a yeni bir finansal hesaplama modülü eklerken, aklıma ilk gelen şey her zamanki gibi daha “sağlam”, daha “geleceğe dönük” bir mimari kurmaktı. Yani hemen bir API gateway, ayrı bir microservice, belki bir message queue… Sonra durdum. Kendi kendime “Mustafa, bu sadece tek bir hesaplama, abartma” dedim.
Bu iç ses, benim gibi kendi küçük projeleriyle uğraşan herkesin sık sık duyduğu bir ses olmalı. Özellikle teknik geçmişi olan bizler için, bir problemi çözmek için hemen en karmaşık, en “best practice” çözümlere yönelmek çok kolay. Ama küçük, indie projelerde bu overengineering çizgisi nerede çekilmeli? Bu konuda kendi tecrübelerimi ve düşüncelerimi paylaşmak istiyorum.
Overengineering Belirtileri: Ne Zaman Durmalı?
Kendi VPS’imde zaten 13 container yönetiyorum; PostgreSQL, Redis, Next.js uygulamaları, bu blogun kendisi. Bazen yeni bir feature için “hadi şuna bir de Kafka ekleyelim” diyesim geliyor. Ama biliyorum ki o Kafka cluster’ı sadece benim için ekstra iş yükü ve bakım demek. Bu, overengineering’in ilk belirtilerinden biri: Mevcut araçlarla çözülebilecek bir problemi daha karmaşık bir araçla çözmeye çalışmak.
Hatta bir keresinde, bu blog için AI generation pipeline’ında yeni bir feature eklerken, “şunu bir de Kubernetes’e taşıyalım, daha resilient olur” diye düşündüm. Neyse ki o an durdum, çünkü 2.5 GB RAM yiyen bir Astro build’i bile sistemimi zorlarken, o karmaşıklık beni ve sunucumu yutabilirdi. Bu tür bir “çözüm”, aslında var olmayan bir problemi çözmeye çalışmak demektir. Benim AI pipeline’ım günde birkaç kez çalışıyor, Kubernetes’in getireceği operasyonel maliyet bu kullanım senaryosu için tamamen abesle iştigal olurdu.
Maliyetler ve Trade-off’lar
Overengineering’in, özellikle tek kişilik veya küçük ekipli projeler için, ciddi maliyetleri var. Bu maliyetler sadece para değil, çok daha değerli şeyler: zaman, enerji ve esneklik.
Zaman Maliyeti
Daha karmaşık bir mimari, daha fazla kod yazma, daha fazla test, daha fazla debug ve daha fazla entegrasyon anlamına gelir. Benim zaten dört yan ürünüm var (hesapciyiz.com, spamkalkani.com, islistesi.com, bu blog). Her birine bu kadar zaman ayıramam. Geçen ay sleep 360 yazıp OOM-killed olduğumda, o basit hatayı bulmak bile zamanımı almıştı. Daha kompleks bir sistemde bu, bir kabusa dönüşebilir ve sizi projeden soğutabilir.
Operasyonel Maliyet
Karmaşık altyapılar daha fazla izleme, daha fazla bakım ve daha fazla sorun giderme gerektirir. Benim kendi VPS’imde 28 Nisan’da disk %100 dolduğunda yaşadığım Docker disk yangını gibi senaryolar, basit bir sistemde bile baş ağrısı yaratabiliyor. 33 GB build cache ve 23 GB unused image yüzünden %100 dolan bir disk, sizin daha karmaşık bir sistem kurduğunuzda karşılaşacağınız binlerce sorundan sadece biri. Bu operasyonel yükü tek başınıza sırtlamak, projenizin ilerlemesini engeller.
Esneklik Kaybı
Erken aşamada yapılan “geleceğe dönük” tasarımlar, aslında gelecekteki değişiklikleri zorlaştırabilir. Henüz iş modeliniz oturmamışken veya kullanıcılarınızın neye ihtiyaç duyduğunu tam olarak bilmezken, büyük mimari kararlar almak sizi kilitler. Bir özellik veya yön değişikliği, devasa bir refactoring projesine dönüşebilir. Bu, “pivot” yapma kabiliyetinizi ciddi şekilde sınırlar.
Benim Yaklaşımım: Basitlik ve Pragmatizm
Kendi projelerimde overengineering tuzağına düşmemek için benimsediğim birkaç temel prensip var. Bunlar, tecrübelerimle, hatalarımla ve “olur o kadar” diyerek öğrendiğim şeyler.
Minimum Viable Architecture (MVA)
Benim için kural şu: Sadece şu anki ihtiyacı karşılayacak kadar bir mimari kurarım. Örneğin, hesapciyiz.com’da, veritabanı olarak PostgreSQL yerine SQLite kullanmamın temel sebebi bu. Evet, PostgreSQL daha güçlü, ama SQLite’ın operasyonel yükü sıfır. Benim için bu bir ‘trade-off’tu’: güçlü özellikler yerine, operasyonel kolaylığı seçtim. Projenin ilk aşamalarında, performansa veya ölçeklenmeye takılmak yerine, temel işlevselliği mümkün olan en basit yolla hayata geçirmeye odaklanıyorum.
İhtiyaç Odaklı Gelişim
“Bugün bu problemi çözmek için ne kadarlık bir mimariye ihtiyacım var?” sorusu, her zaman aklımın bir köşesinde durur. Bir özelliği geliştirirken, hemen en karmaşık çözüme atlamak yerine, mevcut sistemimde nasıl daha basit bir şekilde entegre edebileceğimi düşünürüm. Örneğin, bu blogun kendisi Astro, Node, SQLite ve Nginx ile çalışıyor. Neden bu kombinasyon? Çünkü bu benim için hem basitliği, hem kontrolü, hem de yeterli performansı sağlıyor. Gereksiz bir karmaşıklık eklemektense, tanıdığım ve kolayca yönetebileceğim araçlara odaklanırım.
Incremental Refactoring
İleride ihtiyaç olursa, o zaman refactor ederim. Kendi AI destekli içerik pipeline’ımda ilk başta çok basit bir cron job ile başladım. Bu basit yapı, ilk başta işimi gördü. Daha sonra ihtiyaçlar arttıkça, GitHub Actions ve Cloudflare Workers ile daha sağlam bir yapıya evrildi. Bu süreç ihtiyaç doğdukça ilerledi, baştan her şeyi düşünmedim. Hatta GitHub Actions kotasını delmemek için kendi VPS’imde runner çalıştırmak, benim için hem maliyet hem de kontrol avantajı sağladı. Bu, self-hosted runner ekonomisi konusundaki tecrübelerimin bir sonucuydu.
Karar Verme Süreci: Bir Check-List
Yeni bir özellik veya mimari karar alırken kendime sorduğum birkaç soru var. Bu sorular, beni overengineering’den korumaya yardımcı oluyor:
- Bu özellik gerçekten şu anki problemimi çözüyor mu? Yoksa sadece “olursa iyi olur” kategorisinde mi?
- Bu ek karmaşıklık, operasyonel yükümü ne kadar artıracak? Mevcut sistemimle ne kadar uyumlu?
- Daha basit bir alternatif var mı? Belki daha az “elegant” ama daha pratik bir çözüm?
- Gelecekteki ölçeklenme ihtiyacı ne kadar acil ve kesin? Gerçekten bir gün milyonlarca kullanıcıya ulaşacak mıyım, yoksa bu sadece bir varsayım mı?
- Bu yatırımın geri dönüşü ne olacak? Ekstra zaman ve efora değecek mi?
Sonuç
Küçük projelerde overengineering ile mücadele, benim için sürekli devam eden bir süreç. Bazen kendimi yine bir distributed cache veya karmaşık bir event bus düşünürken buluyorum. Ama sonra kendimi frenleyip, “bir nefes al, en basit yolu düşün” diyorum. Bu yolculukta hatalar da yaptım, yapıyorum da. GitHub Actions runner state corruption’ı yüzünden _work/_temp içindeki dizinleri silmenin acısını da yaşadım, AI pipeline’ında tag’da slash, publishDate’te tırnaklı string gibi AI quirk’leriyle de boğuştum. Önemli olan, bu hatalardan ders çıkarıp bir sonraki projede daha bilinçli adımlar atmak.
Peki sen kendi küçük projelerinde bu overengineering çizgisini nerede çekiyorsun? Ya da benim gibi benzer bir “disk yangını” tecrüben oldu mu? Yorumlarda duymak isterim.