Kurumsal yazılım geliştirme, özellikle ERP gibi omurga sistemlerde çalışmak, insanı ister istemez belirli bir düşünce yapısına sokuyor. Yıllardır bir üretim firmasının ERP’sinde, satın almadan üretime, sevkiyattan faturalamaya kadar sayısız akışı kodladım ve yönettim. Bu süreçlerde onlarca modülün birbiriyle nasıl konuştuğunu, küçük bir değişikliğin başka bir modülde nasıl domino etkisi yarattığını defalarca gördüm. Bu karmaşık entegrasyonların anatomisini çözmek, sadece işimde değil, farkında olmadan kendi yan ürünlerimde de bir alışkanlık haline geldi.
Bu alışkanlık, bazen işleri aşırı karmaşıklaştırmama neden olsa da, çoğu zaman sistemlerin derinlemesine nasıl çalıştığına dair kıymetli bir içgörü de sağlıyor. Kurumsal dünyanın o ağır ve detaylı düşünce yapısı, kendi küçük projelerime sızdığında ortaya çıkan durumları ve bundan çıkardığım dersleri paylaşmak istiyorum. Bu, sadece teknik bir konu değil, aynı zamanda profesyonel deneyimlerin kişisel yaratıcılık üzerindeki etkisine dair bir gözlem.
Modülerlik Mitleri ve Kurumsal Gerçekler
Bir ERP sisteminde “modülerlik” kelimesi, çoğu zaman kulağa hoş gelse de, gerçeklikte bu kavramın sınırları oldukça bulanıktır. Finans, Stok, Üretim, Satış gibi modüllerin her biri kendi içinde bağımsız gibi dursa da, aslında sürekli ve derin bir veri alışverişi içindedirler. Bir stok hareketinin anında muhasebe fişi yaratması, bir üretim emrinin malzeme stoklarını tüketmesi veya bir satış siparişinin sevkiyat planlamayı tetiklemesi, bu görünmez bağların sadece birkaç örneği. Bu bağlar, sistemin tutarlılığı için kritik öneme sahip.
Benim üretim ERP’sinde en çok karşılaştığım durumlardan biri, bir modüldeki performans darboğazının, başka bir kritik iş akışını tamamen kilitlemesiydi. Örneğin, bir dönem Stok Yönetimi modülünde yapılan envanter sayım işlemleri, Muhasebe modülünde fişlerin oluşmasını ciddi şekilde geciktiriyordu. Bu durum, özellikle ay sonu kapanışlarında kritik finansal raporların zamanında alınamamasına yol açıyordu. Sorunun kökeni, stok hareketlerinin doğrudan, tek bir INSERT işlemiyle muhasebe fişi tablosuna tetiklenmesi ve bu tablonun devasa boyutlara ulaşmış, doğru indekslenmemiş olmasıydı.
-- Normalde hızlı olması beklenen bir işlem, büyük tablolarda yavaşlayabiliyordu.
INSERT INTO muhasebe_fisleri (
fis_tarihi, belge_no, aciklama, borc_tutari, alacak_tutari, hesap_kodu, referans_id, referans_tipi
)
SELECT
s.hareket_tarihi, s.belge_no, 'Stok Hareketi ' || s.hareket_kodu, s.tutar, 0,
CASE WHEN s.hareket_tipi = 'GIRIS' THEN '153' ELSE '600' END,
s.id, 'STOK_HAREKET'
FROM
stok_hareketleri s
WHERE
s.muhasebelesme_durumu = 'BEKLIYOR' AND s.hareket_tarihi < CURRENT_DATE
-- Ve bu sorgu her çalıştığında yüzbinlerce satırı tarıyordu.
;
Bu tarz sorunlar, modüllerin sadece API seviyesinde değil, veritabanı şeması ve iş kuralları seviyesinde de ne kadar iç içe geçtiğini gösteriyor. Çözüm, basit bir ALTER INDEX veya VACUUM ANALYZE ile gelmedi; transaction outbox pattern’ini kullanarak stok hareketlerinin muhasebeleştirme işlemini asenkron hale getirmek zorunda kaldım. Bu, modüllerin bağımlılığını azaltırken, sistemin genel performansını ve yanıt verebilirliğini önemli ölçüde artırdı. Modülerlik bir illüzyon olmasa da, derinlemesine entegrasyonun getirdiği zorlukları anlamak, bu alandaki en büyük derslerden biriydi.
Yan Ürünlerime Sızan Kurumsal Mantık
Kurumsal ERP dünyasında kazandığım bu “derinlemesine entegrasyon” bakış açısı, ister istemez kendi yan ürünlerime de sızıyor. Basit bir görev yönetim uygulaması geliştirirken bile, her görevin bir tag ile ilişkilendirilmesi, bu tag’in bir projeye ait olması, projenin de bir müşteriye veya kategoriye bağlanması gibi karmaşık bir hiyerarşi kurma eğilimim oluyor. Sanki bu, gelecekteki “raporlama” veya “analiz” ihtiyaçları için kaçınılmazmış gibi geliyor.
Mesela, kendi görev yönetim uygulamamın backend’ini yazarken, basit bir görev oluşturma işlemi için bile, ileride event sourcing yapabilmek adına bir event tablosu tasarladım. task_created eventi, task_updated eventi… sanki on binlerce kullanıcısı olacakmış gibi. Henüz ben tek başıma kullanırken bile, bir görevin durumunu değiştirmek için bir event fırlatıp, sonra bu event’i bir event handler ile işleyip ana tabloyu güncelleme döngüsüne girmiştim.
# Görev oluşturma servisi - başlangıçta gereksiz karmaşık bir yapı
from datetime import datetime
class TaskCreatedEvent:
def __init__(self, task_id: str, title: str, user_id: str, created_at: datetime):
self.task_id = task_id
self.title = title
self.user_id = user_id
self.created_at = created_at
class TaskService:
def create_task(self, title: str, user_id: str):
task_id = f"task_{datetime.now().timestamp()}"
event = TaskCreatedEvent(task_id, title, user_id, datetime.now())
self._publish_event(event) # Event'i bir kuyruğa veya veritabanına kaydet
print(f"Task created event published for {task_id}")
return task_id
def _publish_event(self, event):
# Gerçekte bu kısım bir veritabanı yazımı veya Kafka kuyruğu olabilirdi.
# Benim durumumda, basit bir SQLite tablosuna JSON olarak kaydediyordum.
pass
# Ana uygulama akışı
task_service = TaskService()
new_task_id = task_service.create_task("Blog yazısını bitir", "mustafa.e")
# Sonra ayrı bir thread/işlem bu eventi alıp ana görevi veritabanına yazacaktı.
Bu yaklaşım, kurumsal yazılımlarda event-driven architecture ve CQRS gibi desenlerin sağladığı faydaları (ölçeklenebilirlik, esneklik, auditing) yan ürünlerime taşımak içindi. Ancak tek kişilik bir projede, bu ek katmanların getirdiği cognitive load ve development overhead çoğu zaman sağladığı faydadan çok daha fazlaydı. Sonunda, basit bir CRUD mimarisine geri döndüğümde hem daha hızlı ilerledim hem de projenin bakımı çok daha kolay oldu. Bu, benim için monolith vs microservice seçimlerinin sadece büyük projeler için değil, kendi küçük projelerim için bile geçerli olduğunu gösterdi.
Entegrasyonun Görünmez Maliyetleri: Teknik Borçtan Fazlası
Kurumsal projelerde yaşadığımız entegrasyon zorlukları, genellikle “teknik borç” kavramı altında tartışılır. Ancak benim için bunun ötesinde, kişisel zaman ve zihinsel enerji maliyetleri de var. Kendi VPS’imde çalışan bir finansal hesaplayıcı yan ürünüm var. İlk başta sadece basit bir kur dönüşümü yapması gerekiyordu. Ancak kurumsal zihniyetle, bunu sadece “kur dönüşümü” olarak bırakmadım. İçine döviz kurları için farklı kaynaklardan veri çekme (TCMB, serbest piyasa, kripto borsaları), geçmiş kur verilerini arşivleme, farklı tarih aralıklarında ortalama kur hesaplama gibi modüller ekledim.
Bu ek modüllerin her biri, kendi içinde bir data source entegrasyonu, API rate limiting yönetimi ve veri tutarlılığı kontrolü gerektiriyordu. Sonuç olarak, basit bir kur dönüşüm API’si, arka planda birden fazla servisle konuşan, birkaç farklı veritabanı (PostgreSQL, Redis cache, zaman serisi için InfluxDB denemeleri) kullanan bir “mini-ERP”ye dönüştü. Doğal olarak, bu karmaşıklık N+1 sorgu problemlerini de beraberinde getirdi. Bir ara, basit bir geçmiş kur sorgusu, benim PostgreSQL sunucumda gözle görülür şekilde yavaşlamıştı. journald loglarına baktığımda, PostgreSQL’in idle in transaction uyarılarıyla dolup taştığını gördüm.
# Süreler ortama ve veri hacmine göre değişir; yavaş sorgular log'da bu biçimde görünür.
my-vps postgres: LOG: execute <unnamed>: SELECT * FROM exchange_rates WHERE currency_pair = $1 AND date BETWEEN $2 AND $3 ORDER BY date ASC;
my-vps postgres: DETAIL: Parameters: $1 = 'USDTRY', $2 = '2025-01-01', $3 = '2025-12-31'
my-vps postgres: LOG: statement: SELECT * FROM exchange_rates WHERE currency_pair = 'USDTRY' AND date BETWEEN '2025-01-01' AND '2025-12-31' ORDER BY date ASC;
Bu log kaydı, benim PostgreSQL performansını iyice optimize etmem gerektiğini gösteriyordu. connection pool tuning yapmam, index stratejilerimi gözden geçirmem (belki BRIN index denemem) ve vacuum monitoring’e daha fazla dikkat etmem gerekti. Bu tür sorunlar, bir production ortamında WAL bloat’a, replication lag’e veya OOM eviction’a yol açabilir. Kendi yan ürünümde bile bu karmaşayı yönetmek zorunda kalmam, kurumsal dünyanın getirdiği bir “mesleki deformasyon” olsa gerek. Daha önce VPS migration sürecinde benzer bir trade-off yaşamıştım; basit bir monolitik yapıyı dağıtık hale getirmeye çalışırken, operasyonel karmaşıklıklar beklenenden çok daha fazla zamanımı almıştı.
Zero-Trust Mimarisi ve Entegrasyon Sınırları
Zero-Trust Architecture (ZTNA) prensipleri, büyük kurumsal ağlarda ve mikroservis mimarilerinde güvenlik için temel bir yaklaşım. “Asla güvenme, her zaman doğrula” mantığı, her modülün veya servisin, aynı ağda dahi olsa, birbirine karşı yetkilendirilmesi ve kimlik doğrulaması yapmasını gerektirir. Bu yaklaşım, ağ güvenliğinde VLAN segmentasyonu, routing authentication ve egress kontrol gibi adımlarla desteklenir.
Ben bu prensipleri, kendi yan ürünlerime bile uygulamaya çalışıyorum. Örneğin, Android spam uygulamamda, farklı iç bileşenlerin (mesaj analiz motoru, numara veritabanı sorgulayıcı, raporlama servisi) birbirleriyle konuşurken belirli API key’ler veya JWT token’lar kullanmasını sağlamaya çalıştım. Evet, bunlar aynı uygulama içinde çalışan bileşenler, ancak kurumsal sistemlerde edindiğim alışkanlık bana her zaman “içeriden gelebilecek tehditleri” de düşünmeyi öğretti.
# Basit bir iç API çağrısı, ancak token ile yetkilendirme girişimi
import requests
def get_spam_score(phone_number: str, token: str):
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(
"http://localhost:8000/api/v1/spam_analysis",
params={"phone": phone_number},
headers=headers
)
response.raise_for_status()
return response.json()["score"]
# Ana uygulama içinde token oluşturma (basit örnek)
internal_api_token = "my_super_secret_internal_token" # Gerçekte JWT/OAuth2 akışı olurdu
score = get_spam_score("5551234567", internal_api_token)
print(f"Spam Score: {score}")
Bu tarz yaklaşımlar, bir müşteri projesinde JWT/OAuth2 paternlerini kullanarak mikroservisler arası iletişimi güvenli hale getirme deneyimimden geliyor. Orada her servis, aldığı isteğin yetkisini Authorization header’daki token’dan doğruluyordu. Bu, rate limiting ve DDoS mitigation katmanlarıyla birlikte, sistemin dış saldırılara karşı daha dirençli olmasını sağlamıştı. Kendi küçük projelerimde bu seviyede bir güvenlik ihtiyacı olmasa da, bu prensipleri uygulamak, hem iyi bir pratik hem de gelecekteki olası büyümelere karşı bir hazırlık gibi hissettiriyor. Ancak tabii ki, bu ek güvenlik katmanları da beraberinde latency ve overhead getirebiliyor, bu da her zaman bir trade-off meselesi.
Basitliğin Gücü: Öğrenilen Dersler ve Gelecek Yaklaşımı
Kurumsal entegrasyon ve mimari işlerinden sonra, kişisel yan ürünlerimde öğrendiğim en büyük ders, çoğu zaman basitliğin karmaşıklıktan daha değerli olduğudur. Her projeye, sanki yarın yüz binlerce kullanıcısı olacakmış gibi event sourcing, CQRS veya microservice mimarileriyle başlamak, çoğu zaman gereksiz bir overhead yaratıyor. Özellikle tek başıma çalıştığım projelerde, bu, development speed’imi düşürüyor ve cognitive load’umu artırıyor.
Artık yeni bir yan ürüne başlarken, ilk önce en basit CRUD (Create, Read, Update, Delete) modelini düşünüyorum. Sadece gerçekten ihtiyaç duyduğumda, örneğin bir mobil uygulamamın belirli bir bölümünde Flutter native bridge üzerinden daha derin bir entegrasyon gerektiğinde veya Play Store yayınlama süreçlerinde spesifik bir metadata reject problemiyle karşılaştığımda, o zaman daha karmaşık çözümlere yöneliyorum. Observability (metrik, log, trace) gibi konuları da başlangıçta çok temel seviyede tutuyorum, sadece kritik hataları yakalayacak kadar.
Mesela, kendi VPS’imdeki Nginx reverse proxy ayarlarımda bile, kurumsal sistemlerden tanıdığım fail2ban kalıplarını kullanıyorum, ancak çok daha basitleştirilmiş bir şekilde:
# Nginx log formatı
log_format main_ext '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'$request_time $upstream_response_time $pipe $upstream_addr '
'$upstream_status $request_id';
# fail2ban için basit bir regex pattern'ı
# /etc/fail2ban/jail.d/nginx-badbots.conf
[nginx-badbots]
enabled = true
port = http,https
filter = nginx-badbots # Kendi custom filter dosyam
logpath = /var/log/nginx/access.log
maxretry = 3
findtime = 600
bantime = 3600
fail2ban için kullandığım nginx-badbots filtresinde, kurumsal ağlarda gördüğüm karmaşık User-Agent veya URL paternlerini değil, sadece çok bariz kötü niyetli botları yakalayan basit regex’ler kullanıyorum. Bu, hem sunucu kaynaklarını daha verimli kullanmamı sağlıyor hem de bakım maliyetini düşürüyor. Bir üretim firmasının ERP’sinde, AI ile üretim planlama modülü üzerinde çalışırken, prompt engineering ve RAG desenlerini kullanmak kaçınılmazdı. Ancak kendi yan ürünlerimde AI uygulama mimarisi söz konusu olduğunda, başlangıçta sadece Gemini Flash veya Groq gibi tek bir sağlayıcıyla başlıyorum, çoklu provider fallback gibi karmaşık yapıları ancak gerçekten ihtiyaç duyulduğunda ekliyorum.
Net pozisyonum şu: Kurumsal dünyanın getirdiği bilgi birikimi ve deneyim paha biçilmez. Ancak bu deneyimi kendi küçük projelerime aktarırken, her zaman trade-off’ları gözetmeli ve over-engineering tuzağına düşmemeliyim. Bazen “olur o kadar” deyip basit bir çözümle ilerlemek, uzun vadede çok daha verimli oluyor. Bir sonraki yazıda, kendi geliştirdiğim anonim bir Türkiye veri platformunun altyapısını kurarken karşılaştığım PostgreSQL partition stratejileri ve vacuum monitoring sorunlarını anlatacağım.