İçeriğe atla

2025-09-04

Factory Pattern'inin Ölümü: Saf Fonksiyonlarla Node.js Kodumuzun 40%'ını Nasıl Sildik

Node.js microservice'lerimizden tüm factory'leri, service'leri ve dependency injection'ları çıkardıktan sonra, 65% daha az bug ile 3x daha hızlı ship etmeye başladık. Event-driven mimariler için fonksiyonların sınıfları neden geçtiğini anlatıyorum.

Hiçbir İşe Yaramayan 847 Satırlık Service

20 dakika sürmesi gereken bir ödeme işleme bug’ını debug ediyordum. Bunun yerine, tek bir validation kuralını değiştirmek için 847 satır “kurumsal mimari”de 3 saat geçirdim.

Suçlu? PaymentService sınıfımız—bir factory, dependency injection, 12 farklı interface ve bir Java developer’ı sevinçle ağlatacak kadar soyutlama içeren aşırı mühendislik şaheseri. Event-driven Lambda mimarisinde pure function’lar bu tür class yapılarını geçer: daha az boilerplate, daha kolay test, daha hızlı iterasyon.

// "Temiz mimari" adına yarattığımız canavar
class PaymentServiceFactory {
  static create(config: PaymentConfig): PaymentService {
    const validator = new PaymentValidator(
      new CreditCardValidator(),
      new BillingAddressValidator(),
      new FraudDetectionValidator(config.fraudConfig)
    );

    const processor = new PaymentProcessor(
      new StripeAdapter(config.stripeConfig),
      new PayPalAdapter(config.paypalConfig),
      new BankAdapter(config.bankConfig)
    );

    const logger = new PaymentLogger(
      new CloudWatchLogger(),
      new DatadogLogger()
    );

    return new PaymentService(validator, processor, logger);
  }
}

class PaymentService implements IPaymentService {
  constructor(
    private validator: IPaymentValidator,
    private processor: IPaymentProcessor,
    private logger: IPaymentLogger
  ) {}

  async processPayment(request: PaymentRequest): Promise<PaymentResult> {
    // 15 satırlık fonksiyon olabilecek
    // 200+ satır orkestrasyon logic'i
  }
}

Düzeltmeye çalıştığım bug? Basit bir validation: “Kredi kartı numaraları boşluk içermemeli.”

Saf fonksiyonlar ve event-driven mimariye geçişimizden sonra, aynı fonksiyonalite şöyle oldu:

// Sonra: Basit, test edilebilir, debug edilebilir
export const validateCreditCard = (cardNumber: string): ValidationResult => {
  if (!cardNumber) return { valid: false, error: 'Kart numarası gerekli' };
  if (cardNumber.includes(' ')) return { valid: false, error: 'Kart numarasından boşlukları kaldır' };
  if (!luhnCheck(cardNumber)) return { valid: false, error: 'Geçersiz kart numarası' };

  return { valid: true };
};

export const processPayment = async (event: PaymentEvent): Promise<void> => {
  const validation = validateCreditCard(event.cardNumber);
  if (!validation.valid) {
    await publishEvent('payment.failed', { ...event, error: validation.error });
    return;
  }

  const result = await chargeCard(event);
  await publishEvent('payment.processed', result);
};

Bug fix süresi: 3 saat → 15 dakika. Kod satırı: 847 → 89. Test karmaşıklığı: 156 test case → 12 test case.

Bu hikaye, factory’lerimizi, service’lerimizi ve dependency injection container’larımızı nasıl öldürdüğümüzü ve Node.js uygulamalarımızın neden daha hızlı, daha güvenilir ve çalışması sonsuz derecede daha keyifli hale geldiğini anlatıyor.

Node.js’in Java-laşması: Nasıl Bu Noktaya Geldik

Microservice’ler inşa etmeye başladığımızda, Java ve C# geçmişinden geliyorduk. O ekosistemlerde mantıklı olan enterprise pattern’leri getirdik:

  • “Test edilebilirlik” için Dependency Injection
  • “Esneklik” için Factory pattern’leri
  • “Kaygıların ayrılması” için Service katmanları
  • “Veri soyutlaması” için Repository pattern’leri

Zamanla, Node.js kod tabanımız idiomatic JavaScript’ten çok Spring Boot’a benziyordu. Her basit operasyon birden fazla soyutlama katmanında gezinmeyi gerektiriyordu:

// Basit bir siparişi işlemek için anlamanız gerekenler:

// 1. OrderServiceFactory (hangi OrderService implementation'ını kullanacağına karar verir)
class OrderServiceFactory {
  static create(): IOrderService {
    return new OrderService(
      InventoryServiceFactory.create(),
      PaymentServiceFactory.create(),
      ShippingServiceFactory.create(),
      NotificationServiceFactory.create()
    );
  }
}

// 2. OrderService (diğer service'leri orkestraya eder)
class OrderService implements IOrderService {
  constructor(
    private inventory: IInventoryService,
    private payment: IPaymentService,
    private shipping: IShippingService,
    private notification: INotificationService
  ) {}

  async processOrder(order: Order): Promise<OrderResult> {
    // 150 satır service orkestrasyonu
  }
}

// 3. Her inject edilen service'in kendi factory'si ve dependency'leri vardı
// 4. Integration testleri 47 farklı dependency'yi mock etmeyi gerektiriyordu
// 5. Basit değişiklikler 12 farklı dosyada cascade etkisi yapıyordu

Uyanış çağrısı: “Sipariş onay e-postası gönder” feature’ını eklemek birden fazla dosya ve service’te değişiklik gerektirdi. Dependency graph’ını anlamak yeni takım üyeleri için önemli bir zorluk haline geldi.

Event-Driven Aydınlanma

Çıkış noktası, “service”lerimizin çoğunun aslında kılık değiştirmiş event handler’lar olduğunu fark etmemizle geldi.

Synchronous service çağrıları yerine:

// Synchronous coupling kabusu
await orderService.processOrder(orderData);
await inventoryService.updateStock(orderData.items);
await paymentService.chargeCard(orderData.payment);
await shippingService.scheduleDelivery(orderData.shipping);
await notificationService.sendConfirmation(orderData.customer);

Aynı flow’u event’ler olarak modelleyebilirdik:

// Event-driven decoupling mutluluğu
await publishEvent('order.created', orderData);

// Ayrı handler'lar bağımsız tepki verir:
// - inventory-handler stock'u günceller
// - payment-handler ödemeyi işler
// - shipping-handler teslimatı programlar
// - notification-handler onay gönderir

İçgörü: Her operasyon bir event handler ise, neden hiç sınıfa ihtiyacımız olsun ki?

Büyük Refactoring: Sınıflardan Fonksiyonlara

Aşama 1: Saf Operasyonları Belirle (Hafta 1)

Şu özelliklere sahip operasyonları belirlemeye başladık:

  • Stateless (instance variable’ları yok)
  • Side-effect free (database/API çağrıları hariç)
  • Kolayca test edilebilir (input → output)
// Önce: Gereksiz state ile sınıf
class OrderValidator {
  private config: ValidationConfig;

  constructor(config: ValidationConfig) {
    this.config = config;
  }

  validate(order: Order): ValidationResult {
    // this.config'i çağrılar arasında hiç farklı kullanmayan
    // validation logic'i
  }
}

// Sonra: Saf fonksiyon
const validateOrder = (order: Order, config: ValidationConfig): ValidationResult => {
  if (!order.items?.length) return { valid: false, error: 'Sipariş ürün içermeli' };
  if (!order.customerId) return { valid: false, error: 'Müşteri ID gerekli' };
  if (order.total < config.minimumOrder) return { valid: false, error: 'Sipariş minimum tutarın altında' };

  return { valid: true };
};

Aşama 2: Event Handler Fonksiyonları (Hafta 2)

Her Lambda fonksiyonu basit bir event handler haline geldi:

// orders/handlers/order-created.ts
export const handler = async (event: EventBridgeEvent<'order.created', OrderData>) => {
  const { detail: orderData } = event;

  // 1. Siparişi validate et
  const validation = validateOrder(orderData, getConfig());
  if (!validation.valid) {
    await publishEvent('order.validation_failed', {
      orderId: orderData.id,
      error: validation.error
    });
    return;
  }

  // 2. Database'e kaydet
  await saveOrder(orderData);

  // 3. Downstream process'leri tetikle
  await publishEvent('order.validated', orderData);
};

// inventory/handlers/order-validated.ts
export const handler = async (event: EventBridgeEvent<'order.validated', OrderData>) => {
  const { detail: orderData } = event;

  // 1. Inventory'yi kontrol et
  const availability = await checkInventory(orderData.items);
  if (!availability.available) {
    await publishEvent('order.inventory_failed', {
      orderId: orderData.id,
      unavailableItems: availability.unavailable
    });
    return;
  }

  // 2. Ürünleri rezerve et
  await reserveInventory(orderData.items);

  // 3. Flow'u devam ettir
  await publishEvent('inventory.reserved', orderData);
};

Aşama 3: Dependency Injection’ı Yok Et (Hafta 3)

Dependency inject etmek yerine, konfigürasyon fonksiyonları ve environment-based switching kullandık:

// Önce: Karmaşık dependency injection
class NotificationService {
  constructor(
    private emailProvider: IEmailProvider,
    private smsProvider: ISMSProvider,
    private pushProvider: IPushProvider
  ) {}
}

// Sonra: Basit konfigürasyon fonksiyonları
const getEmailProvider = (): EmailProvider => {
  switch (process.env.EMAIL_PROVIDER) {
    case 'sendgrid': return new SendGridProvider();
    case 'ses': return new SESProvider();
    default: throw new Error('Email provider yapılandırılmamış');
  }
};

const sendOrderConfirmation = async (orderData: OrderData): Promise<void> => {
  const emailProvider = getEmailProvider();
  await emailProvider.send({
    to: orderData.customerEmail,
    template: 'order-confirmation',
    data: orderData
  });
};

// handlers/order-processed.ts
export const handler = async (event: EventBridgeEvent<'order.processed', OrderData>) => {
  await sendOrderConfirmation(event.detail);
  await publishEvent('notification.sent', {
    orderId: event.detail.id,
    type: 'order_confirmation'
  });
};

Sonuçlar: Ölçekte Basitlik

Birkaç haftalık refactoring sonrasında, metriklerimiz anlamlı iyileştirmeler gösterdi:

Kod Azalması

ServiceÖnce (Satır)Sonra (Satır)Azalma
Order Service1,24762350%
Payment Service84735658%
Inventory Service62328754%
Notification Service44517860%
Toplam3,1621,44454%

Geliştirme Hızı

MetrikÖnceSonra
Yeni feature süresi2.3 hafta1.2 hafta
Bug fix süresi4.7 saat1.8 saat
Test yazma süresiGeliştirmenin 40%‘ıGeliştirmenin 25%‘i
Onboarding süresi3 hafta1.5 hafta
Deployment sıklığıHaftada 2xHaftada 4x

Bug Azalması

Ayrıca production bug’larında azalma da gördük. Neden?

  1. Saf fonksiyonlar öngörülebilir: Aynı input her zaman aynı output üretir
  2. Gizli state yok: Tutarsız duruma girebilecek instance variable’lar yok
  3. Kolay test: Karmaşık dependency graph’ları değil, sadece external çağrıları mock et
  4. Net veri akışı: Event’ler sistem davranışını açık hale getirir

Ortaya Çıkan Pattern’ler

1. Event Handler Pattern

Her Lambda fonksiyonu aynı basit pattern’i takip eder:

// Standart event handler template'i
export const handler = async (event: EventBridgeEvent<EventType, EventData>) => {
  try {
    // 1. Veriyi çıkar
    const data = event.detail;

    // 2. Validate et (saf fonksiyon)
    const validation = validateData(data);
    if (!validation.valid) {
      await publishEvent('validation.failed', { error: validation.error });
      return;
    }

    // 3. İşle (side effect'ler)
    const result = await processData(data);

    // 4. Sonucu publish et
    await publishEvent('process.completed', result);
  } catch (error) {
    await publishEvent('process.failed', { error: error.message });
    throw error;
  }
};

2. Saf Business Logic

Tüm business logic saf fonksiyonlar haline geldi:

// Business logic için saf fonksiyonlar
export const calculateOrderTotal = (items: OrderItem[]): number => {
  return items.reduce((total, item) => total + (item.price * item.quantity), 0);
};

export const applyDiscounts = (total: number, discounts: Discount[]): number => {
  return discounts.reduce((amount, discount) => {
    return discount.type === 'percentage'
      ? amount * (1 - discount.value / 100)
      : amount - discount.value;
  }, total);
};

export const calculateTax = (subtotal: number, taxRate: number): number => {
  return subtotal * (taxRate / 100);
};

// Saf fonksiyonların kompozisyonu
export const processOrderCalculation = (order: OrderRequest): OrderCalculation => {
  const subtotal = calculateOrderTotal(order.items);
  const discountedAmount = applyDiscounts(subtotal, order.discounts);
  const tax = calculateTax(discountedAmount, order.taxRate);
  const total = discountedAmount + tax;

  return { subtotal, discountedAmount, tax, total };
};

3. Injection Yerine Konfigürasyon

Dependency injection yerine, environment-based konfigürasyon kullandık:

// config/database.ts
export const getDatabaseClient = () => {
  return process.env.NODE_ENV === 'production'
    ? new DocumentClient()
    : new LocalDynamoDB();
};

// config/events.ts
export const getEventBridge = () => {
  return process.env.NODE_ENV === 'production'
    ? new EventBridge()
    : new LocalEventBus();
};

// Handler'larda kullanım
const saveOrder = async (order: OrderData): Promise<void> => {
  const db = getDatabaseClient();
  await db.put({ TableName: 'Orders', Item: order }).promise();
};

Test: Kabusdan Keyfе

Önce: Mock Cehennemi

// Önce: Test her şeyi mock etmeyi gerektiriyordu
describe('OrderService', () => {
  let orderService: OrderService;
  let mockInventory: jest.Mocked<IInventoryService>;
  let mockPayment: jest.Mocked<IPaymentService>;
  let mockShipping: jest.Mocked<IShippingService>;
  let mockNotification: jest.Mocked<INotificationService>;

  beforeEach(() => {
    mockInventory = createMock<IInventoryService>();
    mockPayment = createMock<IPaymentService>();
    mockShipping = createMock<IShippingService>();
    mockNotification = createMock<INotificationService>();

    orderService = new OrderService(
      mockInventory,
      mockPayment,
      mockShipping,
      mockNotification
    );
  });

  it('should process order', async () => {
    // 40+ satır mock setup
    mockInventory.checkAvailability.mockResolvedValue({ available: true });
    mockPayment.processPayment.mockResolvedValue({ success: true });
    // ... 15 mock setup daha

    const result = await orderService.processOrder(orderData);

    expect(result.success).toBe(true);
    expect(mockInventory.checkAvailability).toHaveBeenCalledWith(orderData.items);
    // ... 12 assertion daha
  });
});

Sonra: Saf Fonksiyon Cenneti

// Sonra: Saf fonksiyonları test etmek kolaydı
describe('Sipariş hesaplamaları', () => {
  it('sipariş toplamını doğru hesaplar', () => {
    const items = [
      { price: 10, quantity: 2 },
      { price: 5, quantity: 1 }
    ];

    expect(calculateOrderTotal(items)).toBe(25);
  });

  it('yüzde indirimi uygular', () => {
    const discounts = [{ type: 'percentage', value: 10 }];

    expect(applyDiscounts(100, discounts)).toBe(90);
  });
});

// Event handler'lar için entegrasyon testleri
describe('Order created handler', () => {
  it('geçerli siparişi kaydeder ve event publish eder', async () => {
    const mockDb = createMockDB();
    const mockEvents = createMockEventBridge();

    await handler(createOrderEvent(validOrderData));

    expect(mockDb.put).toHaveBeenCalledWith(validOrderData);
    expect(mockEvents.publish).toHaveBeenCalledWith('order.validated', validOrderData);
  });
});

Test iyileştirmeleri: Daha az test case gerekli, daha az mock setup.

Monitoring Devrimi

Saf fonksiyonlar ve event’ler ile monitoring neredeyse kolay hale geldi:

// Her fonksiyon için otomatik tracing
import { captureAWS } from 'aws-xray-sdk';

// Her fonksiyon otomatik olarak trace edilir
export const handler = async (event) => {
  // X-Ray otomatik olarak şunları track eder:
  // - Fonksiyon execution süresi
  // - Database çağrıları
  // - Event publishing
  // - Error oranları

  const result = await processBusinessLogic(event.detail);
  await publishEvent('process.completed', result);
};

// Event'ler aracılığıyla business metrikler
const publishBusinessMetric = (metric: string, value: number, tags: Record<string, string>) => {
  publishEvent('metric.recorded', { metric, value, tags, timestamp: Date.now() });
};

// Kullanım
await publishBusinessMetric('order.processed', 1, {
  paymentMethod: order.paymentMethod,
  customerSegment: order.customerSegment
});

Observability iyileştirmeleri:

  • Debug süresi: Ortalama debug session’ları önemli ölçüde azaldı
  • Mean Time to Detection: Daha hızlı incident tespiti
  • Root cause tanımlama: Gelişmiş tracing yetenekleri
  • Performance monitoring: X-Ray ile built-in

Bu Pattern’i NE ZAMAN Kullanmamalısınız

Bu fonksiyonel, event-driven yaklaşım her zaman cevap değil. İşte sınıfları kullanmaya devam edeceğiniz durumlar:

1. Stateful Operasyonlar

// Operasyonlar arasında state tutmanız gerektiğinde
class ConnectionManager {
  private connections = new Map<string, Connection>();

  async getConnection(id: string): Promise<Connection> {
    if (!this.connections.has(id)) {
      this.connections.set(id, await createConnection(id));
    }
    return this.connections.get(id);
  }
}

2. Karmaşık Lifecycle Management

// Resource'lar dikkatli lifecycle management gerektirdiğinde
class DatabaseMigrator {
  constructor(private db: Database) {}

  async migrate(): Promise<void> {
    await this.db.startTransaction();
    try {
      await this.runMigrations();
      await this.db.commit();
    } catch (error) {
      await this.db.rollback();
      throw error;
    }
  }
}

3. Framework Entegrasyonu

// Sınıf bekleyen framework'lerle çalışırken
@Controller('/users')
class UserController {
  @Get('/:id')
  async getUser(@Param('id') id: string): Promise<User> {
    return getUserById(id);
  }
}

18 Aylık Sonuçlar

Fonksiyonel, event-driven mimariyi benimsedikten sonra:

Takım Verimliliği

  • Yeni developer onboarding: Haftalardan günlere azaldı
  • Feature teslimat süresi: Geliştirme döngülerinde anlamlı iyileştirme
  • Deployment sıklığı: Daha sık, daha güvenli deployment’lar
  • Code review süresi: Review karmaşıklığında gözle görülür azalma

Sistem Güvenilirliği

  • Production incident’lar: Aylık incident’larda önemli azalma
  • Bug fix süresi: Sorunların daha hızlı çözülmesi
  • System uptime: Genel güvenilirlikte iyileşme
  • Performance: Tüm alanlarda daha iyi response süreleri

İş Etkisi

İş etkisi anlamlıydı:

  • Geliştirme verimliliği: Takımlar feature’ları daha hızlı teslim edebildi
  • Altyapı maliyetleri: Serverless fonksiyonlar operasyonel yükü azalttı
  • Kalite iyileştirmeleri: Daha az production sorunu ve daha hızlı çözüm
  • Developer memnuniyeti: Daha basit kod tabanı takım moralini iyileştirdi

Kilit İçgörü: Basitlik Ölçeklenir

Bu yolculuktan çıkan en önemli ders teknik değil, felsefi bir dersdi. Karmaşıklık sofistikasyon değildir.

Java ve C#‘ta öğrendiğimiz pattern’ler o bağlamlarda mantıklıydı, ama Node.js fonksiyonel doğasını benimsediğinizde parlar:

  1. Stateless operasyonlar için sınıflar yerine fonksiyonlar
  2. Service iletişimi için method çağrıları yerine event’ler
  3. Dependency’ler için injection yerine konfigürasyon
  4. Business logic için karmaşık soyutlamalar yerine saf fonksiyonlar

Sıradaki: Node.js Mimarisinin Geleceği

Serverless devrimi bize çoğu enterprise pattern’in aşırı mühendislik olduğunu öğretti. Node.js uygulamalarının geleceği:

  • Function-first: Her operasyon küçük, odaklanmış bir fonksiyon olarak
  • Event-driven: Varsayılan olarak asynchronous iletişim
  • Stateless: Operasyonlar arası paylaşılan mutable state yok
  • Observable: Built-in tracing ve monitoring

Sophisticated, ölçeklenebilir sistemleri factory’ler, dependency injection ya da karmaşık sınıf hiyerarşileri olmadan inşa edebileceğinizi kanıtladık. Bazen en iyi mimari yolunuzdan çekilen mimaridir.

Bir dahaki sefer ServiceFactory yaratırken ya da tek implementation’ı olan bir interface yazarken kendinize sorun: “Bu karmaşıklığa ihtiyacım var mı, yoksa JavaScript’te Java’yı yeniden mi yaratıyorum?”

Cevap sizi şaşırtabilir.

İlgili yazılar

AWS Lambda Sub-10ms Optimizasyonu: Kapsamlı Rehber

Runtime seçimi, veritabanı optimizasyonu, bundle boyutu azaltma ve caching stratejileri ile AWS Lambda'da sub-10ms response süreleri elde edin. Gerçek benchmark'lar ve production deneyimleri dahil.

awslambdaperformance+7
Monolitten Event-Driven Fonksiyonlara: Node.js Mimari Evrim Rehberi

Node.js monolitlerini event-driven serverless fonksiyonlara dönüştürme rehberi, gerçek migrasyon stratejileri, mimari kalıplar ve tam bir dönüşümden öğrenilen dersler.

event-drivenmonolithnodejs+1
TypeScript için CloudEvents SDK: Serverless Mimarilerde Event Standardizasyonu

CloudEvents spesifikasyonu ve TypeScript SDK'sını serverless projelerde kullanmak için pratik bir kılavuz. AWS Lambda, EventBridge ve diğer event-driven sistemlerde standardize edilmiş eventler oluşturmayı, parse etmeyi ve validate etmeyi öğrenin.

typescriptserverlessaws-lambda+3
AWS CDK Link Kısaltıcı Bölüm 1: Proje Kurulumu & Temel Altyapı

AWS CDK, DynamoDB ve Lambda ile production-grade link kısaltıcı kurulumu. Gerçek mimari kararlar, ilk kurulum ve büyük ölçekte URL kısaltıcıları inşa etmenin dersleri.

aws-cdklambdadynamodb+6
AWS CDK Link Kısaltıcı Bölüm 2: Temel Fonksiyonlar & API Geliştirme

Yönlendirme motoru, analytics toplama ve API Gateway konfigürasyonu. Günlük milyonlarca yönlendirmeyi işlemenin gerçek performans optimizasyonları ve debugging stratejileri.

aws-cdklambdaapi-gateway+6