Yazılım dünyasında yıllardır bitmeyen bir kavga var: Mükemmel Mimari vs Çalışan Kod. Tek başına proje geliştiren bir solo yazılımcıysan, bu kavga senin kendi iç dünyanda, her commit öncesinde yaşanır. Tasarım kalıpları, temiz kod prensipleri, mikroservisler derken günün sonunda elimizde tek bir satırı bile production ortamına çıkmamış “mükemmel” ama ölü bir kod yığını kalabiliyor. Yıllar içindeki saha tecrübemde, bu tuzağa bizzat defalarca düştüm; her seferinde de pragmatik olanın, yani çalışan kodun kazandığını gördüm.
Tek başına ürün geliştiren birinin en değerli kaynağı zamandır. Büyük kurumsal ekiplerin haftalarca süren mimari toplantılarla çözdüğü problemleri, senin tek bir gecede çözüp yayına alman gerekir. Bu yazıda, kendi geliştirdiğim yan ürünlerde ve sahada edindiğim tecrübelerle, solo yazılımcıların hayatta kalmasını sağlayacak 3 temel dersi ve mimari kararlardaki trade-off dengelerini inceliyorum.
PostgreSQL index stratejileri
1. Altyapı Tuzağı: Docker Compose Yeten Yere Kubernetes Kurmak
Solo yazılımcıların en sık düştüğü hata, henüz günde 100 kişinin bile ziyaret etmediği bir proje için devasa bir Kubernetes (K8s) cluster’ı ayağa kaldırmaya çalışmaktır. Altyapıyı mükemmelleştirmek, “gerçek işi” yapmaktan kaçmanın en konforlu yoludur. K8s kurarsın, ingress yapılandırırsın, sertifika yenileme otomasyonu için uğraşırsın; ama ortada henüz fatura kesebilen bir ürün yoktur.
Ben kendi yan ürünlerimin birinde ilk başta bu hatayı yaptım. “Yarın bir gün milyonlarca istek gelirse” diyerek çok node’lu bir cluster kurdum. Sonuç ne oldu? Sadece control plane bileşenlerinin RAM tüketimi yüzünden her ay kayda değer bir fatura ödedim. Üstelik bir gün etcd bozulduğunda sistemi ayağa kaldırmak saatlerimi aldı. Oysa aynı işi tek bir VPS üzerinde çalışan basit bir Docker Compose dosyasıyla sıfır overhead ile çözebilirdim.
# docker-compose.yml - Solo yazılımcının can kurtaranı
version: '3.8'
services:
web:
image: python:3.11-slim
command: uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
volumes:
- .:/app
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/dbname
restart: always
deploy:
resources:
limits:
cpus: '0.50'
memory: 512M
db:
image: postgres:16-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: dbname
restart: always
volumes:
postgres_data:
Yukarıdaki basit yapılandırma, saniyede yüzlerce isteği rahatlıkla karşılar. Üstelik cgroup limitleri sayesinde web servisinin RAM sızıntısı yapıp tüm sunucuyu kilitlemesini de engellemiş olursun. Solo yazılımcı için mükemmel altyapı, bakımı en az zaman alan ve gece seni uykundan uyandırmayacak altyapıdır.
2. Veritabanı Tasarımında Aşırı Normalizasyon ve ORM Tuzakları
Akademik kitaplarda anlatılan 3NF, 4NF gibi veritabanı normalizasyon kuralları kağıt üzerinde harika durur. Ancak pratikte, solo bir yazılımcı için aşırı normalize edilmiş bir veritabanı şeması, sonu gelmez JOIN sorguları ve performans darboğazları demektir. Özellikle bir ORM (Object-Relational Mapping) kütüphanesi kullanıyorsan, arka planda dönen N+1 sorgu facialarını fark etmen zaman alabilir.
Bir üretim ERP’si üzerinde çalışırken, her şeyi o kadar çok tablolara bölmüştük ki, operatör ekranında basit bir sipariş durumunu göstermek için çok sayıda tabloyu birleştirmek gerekiyordu. SQL sorguları şişti, veritabanı CPU kullanımı tavan yaptı. Sonrasında bazı verileri denormalize ederek, yani sık okunan alanları JSONB olarak tek bir tabloda tutarak bu sorunu çözdük.
Aşağıdaki SQL örneğinde, karmaşık ilişkileri tek bir sorguda PostgreSQL’in JSON yetenekleriyle nasıl çözebileceğimizi görebilirsin. Bu yaklaşım, kod tarafında onlarca satır ORM kodu yazmaktan ve veritabanını yormaktan seni kurtarır:
-- Tek sorguda ilişkili verileri JSON olarak çekmek (N+1 çözüm)
SELECT
orders.id,
orders.status,
json_build_object(
'id', customers.id,
'name', customers.name,
'email', customers.email
) AS customer,
(
SELECT json_agg(json_build_object('item_id', order_items.product_id, 'qty', order_items.quantity))
FROM order_items
WHERE order_items.order_id = orders.id
) AS items
FROM orders
LEFT JOIN customers ON orders.customer_id = customers.id
WHERE orders.status = 'pending'
LIMIT 50;
Bu sorgu, tek bir veritabanı turu (round-trip) ile ihtiyacın olan tüm veriyi hiyerarşik olarak getirir. Solo yazılımcı olarak senin amacın, kodda yüzlerce satır veri dönüştürme mantığı yazmak yerine, veritabanının gücünü sonuna kadar kullanmaktır.
3. Erken Optimizasyon ve “Belki Lazım Olur” (YAGNI) Mimarileri
“YAGNI” (You Aren’t Gonna Need It) yani “Buna ihtiyacın olmayacak” prensibi, solo yazılımcının duvarına asması gereken en önemli kuraldır. Gelecekteki hayali senaryolar için bugünden kod yazmak, projenin bitmesini engelleyen en büyük etkendir. Örneğin; arka planda çalışacak basit bir e-posta gönderim işlemi için hemen RabbitMQ veya Kafka gibi mesaj kuyruğu sistemlerini kurmaya kalkışmak tam bir zaman kaybıdır.
Örneğin kullanıcı kayıt olduktan sonra gönderilecek hoş geldin e-postası için hemen ayrı bir worker servisi kurmak cazip gelir. Ama önce şunu sormak gerekir: “Şu an günde kaç kişi kayıt oluyor?” Cevap çoğu zaman avuç içi kadar bir sayıdır. Bu işlem için neden ekstra bir servis, kuyruk mekanizması ve log izleme katmanı yönetilsin ki? Bunun yerine Linux’un yerleşik gücünden yararlanarak, basit bir systemd timer veya uygulama içi asenkron bir görev (background task) ile işi çözmek yeterlidir.
| Özellik | Ağır Kuyruk Mimarisi (Kafka/RabbitMQ) | Basit Systemd Timer / Async Task |
|---|---|---|
| Kurulum Süresi | Uzun (saatler) | Çok kısa (dakikalar) |
| Bellek Tüketimi | ~512MB - 1GB RAM | ~0MB (Ekstra yük getirmez) |
| Bakım Maliyeti | Yüksek (Sürüm güncellemeleri, disk doluluğu) | Yok denecek kadar az |
| Solo Geliştirici Uyumu | Düşük | Çok Yüksek |
Eğer sistemin gerçekten büyürse, o zaman mimariyi değiştirmek zaten tatlı bir problem olacaktır. Ama ilk günden bu yükü sırtlanmak, daha yola çıkmadan yorulmana neden olur.
4. Mikroservis Hayalleri vs Monolit Gerçekliği
Mikroservis mimarisi, büyük organizasyonların ekipleri birbirlerinden bağımsız çalıştırabilmek için bulduğu bir çözümdür. Tek başına çalışan bir yazılımcının mikroservis mimarisi seçmesi ise teknik bir intihardır. Servisler arası iletişim, distributed tracing, ağ gecikmeleri (network latency) ve veri tutarlılığı (eventual consistency) gibi problemlerle tek başına baş edemezsin.
Onlarca mikroservise bölünmüş büyük bir platformun dağıtımını ve izlenmesini ayakta tutmanın yarattığı karmaşa, kendi projelerimde neden her zaman “modüler monolit” yaklaşımını seçtiğimi iyi anlatır. Tek bir kod deposu (monorepo), tek bir veritabanı ve tek bir deploy süreci sana inanılmaz bir hız kazandırır.
# /etc/systemd/system/my-monolith-app.service
# Solo yazılımcının monolit servisini canlıda tutan basit systemd tanımı
[Unit]
Description=My Monolith Web Application
After=network.target postgresql.service
[Service]
User=www-data
WorkingDirectory=/var/www/my-app
ExecStart=/var/www/my-app/venv/bin/gunicorn -w 4 -b 127.0.0.1:8000 main:app
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
Yukarıdaki systemd servis tanımı sayesinde, uygulaman herhangi bir sebeple çökerse (örneğin bellek sınırını aşarsa veya beklenmeyen bir hata alırsa) 5 saniye içinde otomatik olarak yeniden başlatılır. Ne Kubernetes’e ihtiyacın var ne de karmaşık pod yönetim araçlarına. Sadece systemd 252+ ve temiz bir konfigürasyon.
5. Hata Yönetimi ve Loglama: Sentry Yokken Journald ile Yetinmek
Gözlemlenebilirlik (observability) önemlidir, ancak bunun için de bütçeni ve zamanını harcamak zorunda değilsin. Ücretli APM araçları veya karmaşık ELK (Elasticsearch, Logstash, Kibana) yığınları yerine, Linux kernel’ının sunduğu yerleşik araçları kullanmayı öğrenmek solo yazılımcı için büyük bir güçtür.
Sistem yönetimi yaparken gördüğüm en büyük hatalardan biri, logların kontrolsüzce büyüyerek diski doldurmasıdır. “Docker disk yangını” dediğimiz, /var/lib/docker/containers dizininin şişerek tüm sunucuyu kilitlemesi olayı neredeyse her solo geliştiricinin başına gelir. Bunu engellemek için journald’nin rate limit ve disk boyutu sınırlandırma özelliklerini kullanabilirsin.
# /etc/systemd/journald.conf.d/size-limit.conf
# Logların diski doldurmasını engellemek için yapılandırma
[Journal]
SystemMaxUse=2G
SystemMaxFileSize=200M
MaxRetentionSec=1month
RateLimitIntervalSec=30s
RateLimitBurst=10000
Bu basit yapılandırma sayesinde, logların toplamda asla 2 GB’tan fazla yer kaplamaz ve saniyede 10.000’den fazla log üreterek sistemi kilitlemeye çalışan servisleri otomatik olarak sınırlar. Bir hata olduğunda ise terminalden tek bir komutla son 1 saatteki kritik hataları süzebilirsin:
# Son 1 saatteki sadece "error" ve daha kritik seviyedeki logları gösterir
journalctl -u my-monolith-app.service --since "1 hour ago" -p err..emerg --no-pager
Docker disk doluluğu çözümleri
6. Solo Yazılımcı İçin Karar Matrisi: Ne Zaman “Mükemmel” Demeli?
Peki, hangi noktada durup kod kalitesine veya mimariye yatırım yapmalıyız? Bunun cevabı çok basit: Kodun ürettiği değer, bakım maliyetini geçtiği an. Eğer yazdığın kod yarın sabah müşteriye fatura kesebiliyorsa, o kod “çalışan” koddur ve o aşamada mükemmel olmasına gerek yoktur.
Aşağıdaki karar matrisi, bir özelliği geliştirirken nasıl bir yol izlemen gerektiği konusunda sana pratik bir rehber sunabilir:
| Senaryo / İhtiyaç | İlk Yaklaşım (Çalışan Kod) | Ne Zaman Değişmeli? (Mükemmel Mimari) |
|---|---|---|
| Kullanıcı Kayıt / Auth | Basit JWT / Session tabanlı tek bir fonksiyon | Kullanıcı sayısı 50.000+ olduğunda veya OAuth2/SSO gerektiğinde |
| Dosya Yükleme | Sunucu diskine doğrudan kayıt (/uploads) |
Disk dolmaya başladığında veya CDN entegrasyonu gerektiğinde (S3 vb.) |
| Raporlama / Analiz | PostgreSQL üzerinde COUNT ve GROUP BY |
Raporların çalışması 5 saniyeden uzun sürdüğünde (Read Replica veya ClickHouse) |
| Ödeme Entegrasyonu | Tek bir API endpoint’i üzerinden doğrudan çağrı | Günlük işlem hacmi $10.000+ olduğunda ve Idempotency gerektiğinde |
Benim bu konudaki net pozisyonum şudur: Önce çalıştır, sonra doğrula, en son optimize et. Çalışmayan bir kodun mimarisi ne kadar mükemmel olursa olsun, değeri sıfırdır. Solo yazılımcı olarak senin asıl gücün, büyük şirketlerin bürokrasiye kurban ettiği o “çevikliği” ve “hızı” kendi lehine kullanmandır. Bırak kodun biraz kirli kalsın, yeter ki production’da olsun ve müşteriye değer üretsin.
Bir sonraki yazıda, kendi geliştirdiğim yan ürünlerin backend mimarisinde kullandığım PostgreSQL optimizasyon tekniklerini anlatacağım.