Yalnız çalışan, tüm sistemi tek başına sırtlayan bir geliştiriciyseniz, önünüzde her sabah iki yol belirir: Kitaplarda yazan kusursuz, pırıl pırıl “clean code” prensiplerine uymak ya da o gün yayına girmesi gereken o özelliği (feature) ne pahasına olursa olsun çalıştırmak. Ben yıllardır bu ikilemin tam ortasında yaşıyorum. Yıllarca kurumsal projelerde, kendi yan ürünlerimde ve üretim ERP’lerinde kod yazarken bu iki kutup arasında gidip geldim.
Sonunda vardığım yer pragmatizmdir. Tek tabanca (solo) çalışan bir geliştiricinin en büyük düşmanı rakipleri ya da yetersiz sunucu kaynakları değil; kendi kafasında yarattığı, gerçeklikle bağı kopmuş “mükemmel mimari” hayalidir. Eğer tek başınaysanız, yazdığınız kodun temizliği ancak o kod üretime (production) girip fatura kesebildiği sürece değerlidir.
1. Mükemmeliyetçilik Tuzağı: Saatlerce Refactoring vs. Hızlıca Production
Sektörde popüler olan “Clean Code” kitaplarının neredeyse tamamı, arkasında devasa bütçeler ve düzinelerce mühendis olan ekipler için yazılmıştır. Tek başınıza çalışırken, her şeyi soyutlama (abstraction) katmanlarının arkasına gizlemek, her küçük fonksiyon için üç katmanlı mimari kurmak sizi sadece yavaşlatır. Geçen yıl kendi geliştirdiğim bir yan ürünün backend tarafında basit bir e-posta gönderim modülü yazmam gerekti. Önümde iki seçenek vardı: Ya tam anlamıyla SOLID prensiplerine uygun, yarın öbür gün e-posta sağlayıcısını değiştirdiğimde tek satır değiştirmem gerekmeyecek soyut bir EmailServiceProvider arayüzü (interface) ve bunun fabrika (factory) sınıflarını yazacaktım ya da doğrudan kütüphaneyi çağırıp geçecektim.
İlk seçeneği seçseydim mimariyi tasarlamak, testlerini yazmak ve bağımlılıkları (dependencies) yönetmek ciddi bir zaman alacaktı. Ben ikinci yolu seçtim; doğrudan kütüphaneyi import edip, API anahtarını çevre değişkeninden (environment variable) okuyan 10 satırlık bir fonksiyon yazdım ve üretime aldım. Toplam süre kısacıktı.
# Aşırı mühendislik (Over-engineered) yaklaşımı - Tek kişi için zaman kaybı
class IEmailProvider(ABC):
@abstractmethod
def send(self, to_email: str, subject: str, body: str): pass
class ComplexEmailFactory:
@staticmethod
def get_provider(provider_type: str) -> IEmailProvider: ...
# Benim tercih ettiğim pragmatik yaklaşım (FastAPI / Python)
import httpx
async def send_simple_email(to_email: str, subject: str, content: str):
# Tek başınaysan, doğrudan hedefe git. Sağlayıcı değişirse bu 10 satırı değiştirirsin.
async with httpx.AsyncClient() as client:
response = await client.post(
"https://api.mailgun.net/v3/mydomain.com/messages",
auth=("api", "key-xxxxxxxxxxxx"),
data={"from": "Mustafa <[email protected]>", "to": to_email, "subject": subject, "text": content}
)
response.raise_for_status()
Bu sade yaklaşım sayesinde kazandığım saatleri, kullanıcıların doğrudan deneyimlediği ödeme adımlarındaki bir hatayı çözmek için harcadım. Tek kişilik operasyonlarda zaman en değerli sermayedir ve bu sermayeyi hayali esneklikler için harcamak iflasa davetiye çıkarır.
2. Over-Engineering Nasıl Para Kaybettirir? (Yıllık Sunucu ve Zaman Maliyeti)
Mikroservis çılgınlığı başladığında, tek kişilik projelerini bile onlarca mikroservise bölüp, araya mesaj kuyrukları (message queues) koyan ve bunları Kubernetes üzerinde koşturmaya çalışan çok meslektaşımı gördüm. Sonuç ne oldu? Ucuz tek bir VPS (Virtual Private Server) üzerinde rahatça çalışabilecek bir proje, kat kat yüksek bir cloud faturasına ve yönetilemez bir karmaşıklığa dönüştü.
Tek tabanca geliştiricinin hedefi her zaman “en az hareketli parça” olmalıdır. Her yeni servis, her yeni veritabanı, her yeni kuyruk mekanizması; izlenmesi (monitoring), güncellenmesi, yedeklenmesi ve çökme anında müdahale edilmesi gereken birer yük demektir.
| Mimari Yaklaşım | Aylık Sunucu Maliyeti | Güncelleme/Bakım Yükü | Hata Tespit Süresi (MTTR) |
|---|---|---|---|
| Gereksiz Mikroservisler | Yüksek (Cloud + Managed DB + K8s) | Ağır | Uzun (Log izleme karmaşası) |
| Pragmatik Monolith | Düşük (1 Core, 2GB RAM VPS) | Hafif | Kısa (Tek bir journald logu) |
Eğer sisteminiz saniyede on binlerce istek almıyorsa (ki projelerin çok büyük çoğunluğu ilk yıllarında bu seviyenin yanına bile yaklaşamaz), tek yapmanız gereken temiz bir monolit yazmaktır. Ben kendi yan ürünlerimde PostgreSQL, FastAPI ve Vue/React üçlüsünü tek bir docker-compose dosyasıyla ayağa kaldırıyorum. Dağıtım (deployment) sadece git pull && docker compose up --build -d komutundan ibaret.
Daha önce kaleme aldığım Docker disk yangınları yazısında bu tarz minimalist altyapılarda karşılaştığım disk dolma sorunlarını ve bunları nasıl çözdüğümü detaylandırmıştım. Altyapıyı basit tutmak, hata anında nereye bakacağınızı bilmenizi sağlar.
3. “Çalışan Kod” Çöp Kod Demek Değildir: Benim Pragmatik Standartlarım
Burada yanlış anlaşılmak istemem; “çalışan kod” derken güvenlik açık barındıran, değişken isimleri a, b, c olan, hiçbir log üretmeyen çöp bir kod yığınından bahsetmiyorum. Tam aksine, tek başınıza olduğunuzda kodun “okunabilir” olması, gelecekteki kendinize borçlanmamanız için hayati önem taşır. Ancak benim temiz kod standartlarım, akademik kitaplardaki kurallardan farklıdır.
Benim için pragmatik temiz kodun üç temel kuralı vardır:
- Okunabilirlik (Readability): Koda 6 ay sonra baktığımda, hiçbir dökümantasyona ihtiyaç duymadan ne yaptığını anlayabilmeliyim. Karmaşık tek satırlık (one-liner) sihirbazlıklar yerine, açık ve uzun değişken isimlerini tercih ederim.
- Hata Yönetimi ve Loglama (Error Handling): Kod patlayabilir, bu doğaldır. Ama patladığında bana tam olarak hangi satırda, hangi veriyle patladığını söyleyen anlamlı bir hata logu üretmelidir.
- Güvenlik (Security): SQL injection, yetkilendirme (authorization) eksiklikleri ve rate limiting gibi temel konular asla “çalışan kod” bahanesiyle ertelenemez.
# OKUNABİLİR VE GÜVENLİ: Basit ama sağlam bir endpoint örneği
@app.post("/api/v1/invoice/generate")
@limiter.limit("5/minute") # Rate limiting hayati önem taşır
async def generate_invoice(request: InvoiceRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
# Yetki kontrolü pragmatik ve nettir
if not current_user.is_active:
logger.warning(f"Pasif kullanıcı fatura üretmeye çalıştı: {current_user.id}")
raise HTTPException(status_code=403, detail="Account is suspended")
try:
invoice = create_invoice_record(db, request, current_user.id)
return {"status": "success", "invoice_id": invoice.id}
except Exception as e:
# Loglama olmadan çalışan kod, saatli bir bombadır
logger.error(f"Fatura üretilemedi. Kullanıcı: {current_user.id}, Hata: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail="Internal billing system error")
Bu kodda karmaşık tasarım desenleri (design patterns) yok, gereksiz soyutlamalar yok. Ama güvenli, log mekanizması kurulu ve okunabilir. İşte tek tabanca geliştiricinin standartı tam olarak bu olmalıdır.
4. Gerçek Bir Vaka: Ağır Süren Sorguyu Hızlandırırken Neyi Feda Ettim?
Bir üretim ERP’si projemde, sevkiyat raporlama ekranı her çalıştığında tüm sistemi kilitliyordu. Sorgu ağır şekilde sürüyordu ve veritabanı CPU kullanımı tavan yapıyordu. Akademik bir “clean code” yaklaşımı benimseseydim, veritabanı şemasını tamamen normalize etmem, eski verileri arşivleyecek bir mikroservis yazmam ve belki de CQRS (Command Query Responsibility Segregation) desenini uygulayarak okuma ve yazma veritabanlarını ayırmam gerekirdi. Bu yaklaşımın bana maliyeti çok uzun bir uğraş ve potansiyel yeni buglar olacaktı.
Ben ne yaptım? Sorunu analiz ettiğimde, sorgunun sürekli geçmiş 30 günlük hareketleri taradığını ve status alanı üzerinde bir indeks olmadığını gördüm. PostgreSQL üzerinde sadece tek bir kısmi indeks (partial index) tanımladım ve sorgunun içindeki gereksiz üç adet JOIN işlemini kaldırarak ham SQL sorgusuna çevirdim.
-- Akademik olarak "temiz" olmayan ama hayat kurtaran kısmi indeks (partial index)
CREATE INDEX idx_delivery_pending_last_30_days
ON deliveries (created_at DESC, customer_id)
WHERE status = 'PENDING' AND created_at > NOW() - INTERVAL '30 days';
Sorgu süresi anında belirgin biçimde düştü ve ekran tekrar kullanılabilir hale geldi. Evet, kodun içine ORM (Object-Relational Mapping) yerine ham SQL sorgusu gömdüm. Bazı yazılım guruları bunu “clean code” standartlarına aykırı bulabilir çünkü veritabanı bağımlılığı yaratıyor. Ancak gerçek dünyada o ERP’nin veritabanını PostgreSQL’den başka bir sisteme taşıma ihtimalimiz sıfıra yakındı. Bu pragmatik karar bana ciddi zaman kazandırdı ve sistemin sorunsuz çalışmasını sağladı. Benzer performans çözümlerini PostgreSQL index stratejileri yazımda da paylaşmıştım; bazen en iyi kod, en az yazılan koddur.
5. Tek Kişilik Devops ve Altyapı Yönetiminde “Yetecek Kadar Temiz” Olmak
Eğer tek başınıza hem kod yazıp hem de o kodun çalıştığı sunucuyu yönetiyorsanız, sistem yönetimi tarafında da pragmatik olmak zorundasınız. CI/CD süreçlerini otomatize etmek için haftalarca uğraşıp karmaşık pipeline’lar kurmak yerine, işinizi görecek en basit ve güvenilir yöntemi seçmelisiniz.
Kendi projelerimde Jenkins, GitLab CI gibi ağır araçlar yerine GitHub Actions veya basit bir self-hosted runner kullanıyorum. Sunucu tarafında ise Docker konteynerlerini yönetmek için Kubernetes yerine systemd ve docker-compose ikilisini tercih ediyorum. Bir servisin çökmesi durumunda otomatik olarak ayağa kalkmasını sağlayan basit bir systemd unit dosyası, bana geceleri huzurlu bir uyku uyutmaya yetiyor.
# /etc/systemd/system/app-watcher.service
[Unit]
Description=My Pragmatic App Container Watcher
After=docker.service
Requires=docker.service
[Service]
TimeoutStartSec=0
Restart=always
RestartSec=10
# Basit bir health check ve otomatik restart mekanizması
ExecStart=/usr/local/bin/docker-compose -f /opt/myapp/docker-compose.yml up
ExecStop=/usr/local/bin/docker-compose -f /opt/myapp/docker-compose.yml down
[Install]
WantedBy=multi-user.target
Bu servis dosyası sayesinde, sunucu kapansa da, Docker daemon çökse de sistem kendi kendini toparlıyor. Karmaşık Kubernetes pod replikasyon ayarlarıyla uğraşmadan, işletim sisteminin kendi yeteneklerini (systemd) kullanarak sıfır maliyetli ve son derece güvenilir bir altyapı elde etmiş oluyorum.
6. Teknik Borç (Technical Debt) Yönetimi: Ne Zaman Ödemeli, Ne Zaman İflas İlan Etmeli?
“Çalışan kod” yazarak ilerlediğinizde, sisteminizde kaçınılmaz olarak teknik borç birikir. Bu borç tıpkı kredi kartı borcu gibidir; işinizi büyütmek için mantıklı bir araçtır ama kontrolsüz büyürse sizi batırır. Tek tabanca geliştirici olarak bu borcu yönetmenin sırrı, ne zaman ödeyeceğinizi iyi bilmektir.
Ben kodun içine yazdığım “geçiştirme” çözümleri mutlaka özel bir formatta etiketlerim. Bu etiketler sıradan bir TODO yorumu değildir. İçinde tarih, borcun nedeni ve bu borcun ne zaman ödenmesi gerektiğine dair bir tetikleyici (trigger) barındırır.
# FIXME: [2026-05-30] - Mustafa Erbay
# NEDEN: İlk lansman için hızlıca JSON dosyasından okuyoruz.
# TETİKLEYİCİ: Aktif kullanıcı sayısı 500'ü geçtiğinde PostgreSQL tablosuna taşınmalı.
# ETKİ: Okuma hızı O(N), 500 kullanıcıdan sonra performans sorunu yaratır.
def get_user_preferences_legacy(user_id: int):
with open("temp_preferences.json", "r") as f:
data = json.load(f)
return data.get(str(user_id), {})
Haftalık veya aylık periyotlarda kod tabanında bu etiketleri aratırım. Eğer tanımladığım tetikleyiciye (örneğin kullanıcı sayısı limitine) ulaşılmadıysa, o koda dokunmam. Çalışıyorsa ve işi görüyorsa bırakın öyle kalsın. Ancak ne zaman ki o borç iş süreçlerini yavaşlatmaya veya gerçek hatalara sebep olmaya başlar, işte o zaman planlı bir refactoring süreci başlatırım.
Tek başınıza başarılı olmak istiyorsanız, mükemmel kod yazma takıntısını bir kenara bırakıp, “değer üreten ve çalışan kod” yazmaya odaklanmalısınız. Çünkü günün sonunda kullanıcılarınız sizin kodunuzun ne kadar temiz olduğuna değil, uygulamanızın onların sorununu çözüp çözmediğine bakar.
Bir sonraki yazımda, tek kişilik projelerde yapay zeka ajanlarını (AI Agents) birer iş gücü olarak nasıl konumlandırdığımı ve operasyonel yükümü belirgin biçimde nasıl azalttığımı anlatacağım.