Skip to content
~/sph.sh

Event-Driven Mimari ile CRM Sistemleri Geliştirmek

Event sourcing, CQRS ve event-driven pattern'leri kullanarak müşteri ilişkileri yönetimi, marketing otomasyonu ve consent yönetimi için pratik bir rehber

Özet

Geleneksel CRM sistemleri gerçek zamanlı kişiselleştirme, karmaşık consent yönetimi ve çok kanallı orkestrasyon konularında zorlanıyor. Event-driven mimari temelden farklı bir yaklaşım sunuyor: müşteri kayıtlarını direkt güncellemek yerine, her etkileşimi değişmez bir event olarak kaydediyorsun. Bu değişim gerçek müşteri 360 görünümü, GDPR uyumlu audit trail'leri ve gerçek zamanlı marketing otomasyonu sağlıyor. Event-driven temellere sahip CRM sistemleri geliştirirken öğrendiklerimi paylaşacağım.

Event-Driven CRM Manzarası

Çoğu CRM sistemi basit başlıyor: müşteriler, kişiler ve etkileşimler için bir veritabanı. Bu şu sorulara cevap vermene kadar işe yarıyor: "Bu müşteriye hangi marketing e-postaları gönderildi?" veya "SMS'e ne zaman onay verdiler?" veya "Neden onlara bu bildirimi gönderdik?"

Geleneksel CRM mimarilerinden event-driven sistemlere geçiş yapan ekiplerle çalıştım ve bu değişim müşteri verisini nasıl modellediğini yeniden düşünmeyi gerektiriyor. Tercihler değiştiğinde müşteri kaydını güncellemek yerine CustomerPreferencesUpdated eventi emit ediyorsun. GDPR için consent kayıtlarını silmek yerine ConsentRevoked eventi emit ediyorsun.

Temel fark: veritabanın event'lerin bir projection'ı oluyor, truth'un kaynağı değil.

Neden CRM için Event-Driven Mimari?

CRM domain'inin event-driven mimariyi özellikle değerli kılan spesifik özellikleri var:

  1. Audit Gereksinimleri: GDPR, consent'in tam olarak ne zaman ve hangi amaçla verildiğini bilmeyi zorunlu kılıyor
  2. Çok Kanallı Karmaşıklık: Müşteriler email, SMS, push, in-app üzerinden etkileşime giriyor ve her kanalın farklı kuralları var
  3. Gerçek Zamanlı Kişiselleştirme: Marketing otomasyonunun müşteri davranışına anında tepki vermesi gerekiyor
  4. Veri Gizliliği: "Unutulma hakkı" event'leri redaction ile replay edebildiğinde daha kolay
  5. Eventual Consistency: Marketing kampanyaları daha iyi ölçeklenebilirlik anlamına geliyorsa hafif gecikmeleri tolere edebilir

Gerçekçi bir senaryo: Müşteri ürün sayfana göz atıyor, sepetini terk ediyor, SMS bildirimlerini aktif ediyor, sonra email linki üzerinden satın alma işlemini tamamlıyor. Geleneksel CRM'de müşteri kaydını birkaç kez güncellersin, event sırasını kaybedersin. Event-driven sistemde tam hikayeye sahipsin.

Sistem Mimarisi Genel Bakış

Core bileşenlerin nasıl bir araya geldiğini göstereyim:

Bu mimari concern'leri etkili şekilde ayırıyor:

  • Write path: Command'lar business rule'ları validate eder ve event'leri emit eder
  • Read path: Projection'lar query'ler için optimize edilmiş view'ları materialize eder
  • Services: Event'lere tepki verir ve workflow'ları orkestra eder
  • Channels: Retry logic ve failure tracking ile delivery'yi handle eder

Pratik Implementasyon Rehberi

Bileşenlere derinlemesine dalmadan önce, gerçek bir implementasyona nasıl başlayacağını göstereyim.

Adım Adım Başlangıç

Adım 1: Core Event'lerini Tanımla

Basit başla. Her şeyi bir anda modellemeye çalışma:

typescript
// Sadece müşteri oluşturma ve consent ile başlaconst coreEvents = [  'CustomerCreated',  'ConsentGranted',  'ConsentRevoked',  'EmailSent'];

Adım 2: Event Store'u Kur

Elinde olanı kullan. DynamoDB AWS ekipleri için, EventStoreDB event sourcing puristleri için iyi çalışıyor:

typescript
// Basit DynamoDB event storeconst eventStoreConfig = {  tableName: 'customer-events',  partitionKey: 'customerId',   // PK: CUSTOMER#{id}  sortKey: 'timestamp_eventId',  // SK: EVENT#{timestamp}#{eventId}  ttl: 7 * 365 * 86400           // 7 yıl retention};

Adım 3: Command Handler'ları Oluştur

Business logic burada yaşıyor:

typescript
// Her aggregate için bir handlerclass CustomerCommandHandler {  async execute(command: Command): Promise<void> {    // 1. Event'leri yükle    // 2. State'i yeniden oluştur    // 3. Business rule'ları validate et    // 4. Yeni event'leri emit et  }}

Adım 4: Projection'ları İnşa Et

Tek bir read model ile başla - müşteri view'ı:

typescript
// Müşteri query'leri için tek projectionclass CustomerProjection {  async handleEvent(event: CustomerEvent): Promise<void> {    switch (event.eventType) {      case 'CustomerCreated':        await this.createCustomer(event);        break;      case 'ConsentGranted':        await this.updateConsent(event);        break;    }  }}

Adım 5: Campaign Trigger'ları Ekle

Basit bir kampanya ile başla - hoş geldin emaili:

typescript
const welcomeCampaign = {  trigger: 'CustomerCreated',  actions: [    { type: 'send-email', template: 'welcome' }  ]};

Adım 6: Kanal'ları Entegre Et

Mevcut provider'ları kullan. Email altyapısı inşa etme:

typescript
// Mevcut email provider'ını wrap etclass EmailChannel {  constructor(private sendGrid: SendGridClient) {}
  async send(customerId: string, template: string): Promise<void> {    // Müşteri verisini projection'dan al    // Provider üzerinden gönder    // EmailSent eventi emit et  }}

Tam Uçtan Uca Örnek

Kayıttan satın alma onayına kadar tam müşteri yolculuğu:

Bu akış için tam kod:

typescript
// 1. Müşteri Kaydıasync function handleRegistration(request: RegistrationRequest): Promise<string> {  const customerId = crypto.randomUUID();
  // CustomerCreated emit et  await eventStore.appendEvent({    eventId: crypto.randomUUID(),    customerId,    timestamp: new Date().toISOString(),    eventType: 'CustomerCreated',    email: request.email,    firstName: request.firstName,    source: 'web'  });
  // Consent verdiyse, ConsentGranted emit et  if (request.marketingConsent) {    await eventStore.appendEvent({      eventId: crypto.randomUUID(),      customerId,      timestamp: new Date().toISOString(),      eventType: 'ConsentGranted',      purpose: 'marketing',      channel: 'email'    });  }
  return customerId;}
// 2. Projection Müşteri Kaydını Güncellerasync function handleCustomerCreated(event: CustomerCreated): Promise<void> {  await customerDB.putItem({    customerId: event.customerId,    email: event.email,    firstName: event.firstName,    status: 'active',    createdAt: event.timestamp  });}
// 3. Hoş Geldin Kampanyası Tetiklenirasync function handleCustomerCreatedCampaign(event: CustomerCreated): Promise<void> {  // Consent'i kontrol et  const hasConsent = await consentService.hasActiveConsent(    event.customerId,    'marketing',    'email'  );
  if (hasConsent) {    await emailChannel.send({      customerId: event.customerId,      templateId: 'welcome-email',      data: { firstName: event.firstName }    });  }}
// 4. Ürün Göz Atımı İzlenirasync function handleProductView(customerId: string, productId: string): Promise<void> {  await eventStore.appendEvent({    eventId: crypto.randomUUID(),    customerId,    timestamp: new Date().toISOString(),    eventType: 'ProductViewed',    productId,    sessionId: getCurrentSessionId()  });}
// 5. Sepet Terki Algılama (periyodik çalışır)async function detectAbandonedCarts(): Promise<void> {  const abandonedCarts = await findCartsWithNoActivity(60); // 60 dakika
  for (const cart of abandonedCarts) {    await eventStore.appendEvent({      eventId: crypto.randomUUID(),      customerId: cart.customerId,      timestamp: new Date().toISOString(),      eventType: 'CartAbandoned',      cartId: cart.cartId,      items: cart.items,      totalValue: cart.total    });  }}
// 6. Sepet Terki Kampanyasıasync function handleCartAbandoned(event: CartAbandoned): Promise<void> {  // Göndermeden önce 1 saat bekle  await scheduleAction({    executeAt: Date.now() + 3600000,    action: async () => {      await emailChannel.send({        customerId: event.customerId,        templateId: 'cart-reminder',        data: {          cartItems: event.items,          cartTotal: event.totalValue        }      });    }  });}
// 7. Sipariş Vermeasync function handleOrderPlacement(request: PlaceOrderRequest): Promise<string> {  const orderId = crypto.randomUUID();
  // OrderPlaced emit et  await eventStore.appendEvent({    eventId: crypto.randomUUID(),    customerId: request.customerId,    timestamp: new Date().toISOString(),    eventType: 'OrderPlaced',    orderId,    items: request.items,    total: request.total  });
  // Ödemeyi işle  const paymentResult = await paymentProvider.charge({    amount: request.total,    customerId: request.customerId  });
  if (paymentResult.success) {    await eventStore.appendEvent({      eventId: crypto.randomUUID(),      customerId: request.customerId,      timestamp: new Date().toISOString(),      eventType: 'PaymentSucceeded',      orderId,      amount: request.total,      transactionId: paymentResult.transactionId    });  }
  return orderId;}
// 8. Sipariş Onay Kampanyasıasync function handlePaymentSucceeded(event: PaymentSucceeded): Promise<void> {  // OrderConfirmed emit et  await eventStore.appendEvent({    eventId: crypto.randomUUID(),    customerId: event.customerId,    timestamp: new Date().toISOString(),    eventType: 'OrderConfirmed',    orderId: event.orderId,    confirmationNumber: generateConfirmationNumber()  });
  // Onay emaili gönder  await emailChannel.send({    customerId: event.customerId,    templateId: 'order-confirmation',    data: {      orderId: event.orderId,      amount: event.amount    }  });}
// 9. Projection Güncellemeleri - Müşteri Artık Alıcıasync function handleFirstPurchase(event: PaymentSucceeded): Promise<void> {  const purchases = await getPurchaseCount(event.customerId);
  if (purchases === 1) {    // İlk satın alma - müşteri segmentini güncelle    await eventStore.appendEvent({      eventId: crypto.randomUUID(),      customerId: event.customerId,      timestamp: new Date().toISOString(),      eventType: 'CustomerSegmentAdded',      segmentId: 'buyers',      segmentName: 'Satın Alan Müşteriler'    });  }}

Bu örnek her eventi, her projection güncellemesini ve her kampanya tetikleyicisini gösteriyor. Buradan başla, sonra daha karmaşık workflow'lara genişlet.

Müşteri Yaşam Döngüsü Event Akışı

Zaman içinde müşteri event'lerinin tam resmi:

Bileşen Derinlemesine İnceleme

Müşteri Verisi için Event Sourcing

Core pattern: mevcut state'i saklamak yerine, o state'e yol açan event dizisini saklıyorsun. İşte pratik bir implementasyon:

typescript
// Event tanımları - truth'un kaynağıinterface CustomerEvent {  eventId: string;  customerId: string;  timestamp: string;  eventType: string;}
interface CustomerCreated extends CustomerEvent {  eventType: 'CustomerCreated';  email: string;  source: 'web' | 'mobile' | 'api';}
interface ConsentGranted extends CustomerEvent {  eventType: 'ConsentGranted';  purpose: 'marketing' | 'analytics' | 'essential';  channel: 'email' | 'sms' | 'push';  ipAddress: string;  userAgent: string;}
interface ConsentRevoked extends CustomerEvent {  eventType: 'ConsentRevoked';  purpose: 'marketing' | 'analytics' | 'essential';  channel: 'email' | 'sms' | 'push';  reason?: string;}
interface PreferencesUpdated extends CustomerEvent {  eventType: 'PreferencesUpdated';  preferences: {    emailFrequency?: 'daily' | 'weekly' | 'never';    categories?: string[];    timezone?: string;  };}

Event store senin single source of truth'un oluyor:

typescript
class EventStore {  constructor(    private dynamoDB: DynamoDBClient,    private eventBus: EventBridge  ) {}
  async appendEvent(event: CustomerEvent): Promise<void> {    // Event'i optimistic locking ile sakla    await this.dynamoDB.putItem({      TableName: 'customer-events',      Item: {        pk: { S: `CUSTOMER#${event.customerId}` },        sk: { S: `EVENT#${event.timestamp}#${event.eventId}` },        eventType: { S: event.eventType },        payload: { S: JSON.stringify(event) },        version: { N: '1' },        ttl: { N: String(Math.floor(Date.now() / 1000) + 7 * 365 * 86400) }      },      ConditionExpression: 'attribute_not_exists(pk)'    });
    // Consumer'lar için event bus'a publish et    await this.eventBus.putEvents({      Entries: [{        Source: 'crm.customer',        DetailType: event.eventType,        Detail: JSON.stringify(event),        EventBusName: 'customer-events'      }]    });  }
  async getCustomerEvents(    customerId: string,    fromTimestamp?: string  ): Promise<CustomerEvent[]> {    const params = {      TableName: 'customer-events',      KeyConditionExpression: 'pk = :pk AND sk >= :sk',      ExpressionAttributeValues: {        ':pk': { S: `CUSTOMER#${customerId}` },        ':sk': { S: fromTimestamp ? `EVENT#${fromTimestamp}` : 'EVENT#' }      }    };
    const result = await this.dynamoDB.query(params);    return result.Items?.map(item =>      JSON.parse(item.payload.S!)    ) ?? [];  }}

Önemli gotcha: Event versioning kritik hale geliyor. Event schema'n evrim geçirdiğinde upcaster'lara ihtiyacın oluyor:

typescript
interface EventUpcaster {  fromVersion: number;  toVersion: number;  upcast(event: any): any;}
// Örnek: Consent event'lerine GDPR context eklemeconst consentEventUpcaster: EventUpcaster = {  fromVersion: 1,  toVersion: 2,  upcast(event: any) {    if (event.eventType === 'ConsentGranted' && !event.gdprContext) {      return {        ...event,        version: 2,        gdprContext: {          legalBasis: 'consent',          retentionPeriod: '2years',          dataController: 'company-name'        }      };    }    return event;  }};

CQRS: Read ve Write'ı Ayırma

CQRS (Command Query Responsibility Segregation) write model'in ve read model'in tamamen farklı olduğu anlamına geliyor. CRM context'inde bu güçlü çünkü marketing query'leri consent validation'dan farklı veri yapıları gerektiriyor.

Write Model - Business rule'lar için optimize edilmiş:

typescript
class CustomerCommandHandler {  constructor(    private eventStore: EventStore,    private validator: BusinessRuleValidator  ) {}
  async grantConsent(command: GrantConsentCommand): Promise<void> {    // Validate etmek için event history'yi yükle    const events = await this.eventStore.getCustomerEvents(command.customerId);    const customer = this.rehydrateCustomer(events);
    // Business rule: Silinmiş müşteri için consent verilemez    if (customer.isDeleted) {      throw new Error('Cannot grant consent for deleted customer');    }
    // Business rule: Aynı consent revocation olmadan iki kez verilemez    const existingConsent = customer.consents.find(      c => c.purpose === command.purpose &&           c.channel === command.channel &&           c.status === 'active'    );
    if (existingConsent) {      throw new Error('Consent already exists');    }
    // Yeni event emit et    await this.eventStore.appendEvent({      eventId: crypto.randomUUID(),      customerId: command.customerId,      timestamp: new Date().toISOString(),      eventType: 'ConsentGranted',      purpose: command.purpose,      channel: command.channel,      ipAddress: command.ipAddress,      userAgent: command.userAgent    });  }
  private rehydrateCustomer(events: CustomerEvent[]): Customer {    // Event'lerden state'i yeniden oluştur - bu event sourcing    return events.reduce((customer, event) => {      switch (event.eventType) {        case 'CustomerCreated':          return { ...customer, email: event.email };        case 'ConsentGranted':          return {            ...customer,            consents: [...customer.consents, {              purpose: event.purpose,              channel: event.channel,              grantedAt: event.timestamp,              status: 'active'            }]          };        case 'ConsentRevoked':          return {            ...customer,            consents: customer.consents.map(c =>              c.purpose === event.purpose && c.channel === event.channel                ? { ...c, status: 'revoked', revokedAt: event.timestamp }                : c            )          };        default:          return customer;      }    }, { consents: [] } as Customer);  }}

Read Model - Query'ler için optimize edilmiş:

typescript
// Projection builder - event bus'tan async çalışırclass ConsentProjectionBuilder {  constructor(private readDB: DynamoDBClient) {}
  async handleConsentGranted(event: ConsentGranted): Promise<void> {    // "Bu müşteriyle iletişim kurabilir miyiz?" için optimize edilmiş materialized view    await this.readDB.putItem({      TableName: 'customer-consents',      Item: {        pk: { S: `CUSTOMER#${event.customerId}` },        sk: { S: `CONSENT#${event.purpose}#${event.channel}` },        status: { S: 'active' },        grantedAt: { S: event.timestamp },        expiresAt: { S: this.calculateExpiry(event.timestamp) },        ipAddress: { S: event.ipAddress },        // Purpose'a göre query için GSI        gsi1pk: { S: `PURPOSE#${event.purpose}` },        gsi1sk: { S: event.customerId }      }    });  }
  async handleConsentRevoked(event: ConsentRevoked): Promise<void> {    await this.readDB.updateItem({      TableName: 'customer-consents',      Key: {        pk: { S: `CUSTOMER#${event.customerId}` },        sk: { S: `CONSENT#${event.purpose}#${event.channel}` }      },      UpdateExpression: 'SET #status = :revoked, revokedAt = :timestamp',      ExpressionAttributeNames: { '#status': 'status' },      ExpressionAttributeValues: {        ':revoked': { S: 'revoked' },        ':timestamp': { S: event.timestamp }      }    });  }
  private calculateExpiry(grantedAt: string): string {    // GDPR makul süre sonra re-consent gerektiriyor    const granted = new Date(grantedAt);    granted.setFullYear(granted.getFullYear() + 2);    return granted.toISOString();  }}

Trade-off: eventual consistency. Müşteri consent'i iptal ettiğinde read model güncellenene kadar bir gecikme oluyor. CRM için bu genellikle kabul edilebilir - müşteri abonelikten çıkarsa kampanyaların durması için birkaç saniye gecikme makul.

Tam CRUD İşlemleri

Temel işlemlerin event'lere nasıl dönüştüğünü anlamak temel. Müşteri veri yönetiminin tam yaşam döngüsünü adım adım inceleyelim.

Müşteri Oluşturma Akışı

Yeni bir müşteri kayıt olduğunda, sadece bir satır eklemiyorsun - bir event stream'i başlatıyorsun:

typescript
interface CustomerRegistrationCommand {  email: string;  firstName: string;  lastName: string;  phone?: string;  source: 'web' | 'mobile' | 'api' | 'import';  marketingConsent: boolean;  termsAccepted: boolean;  ipAddress: string;  userAgent: string;}
class CustomerRegistrationHandler {  constructor(    private eventStore: EventStore,    private validator: EmailValidator  ) {}
  async registerCustomer(    command: CustomerRegistrationCommand  ): Promise<string> {    // Adım 1: Event oluşturmadan önce validate et    await this.validateRegistration(command);
    const customerId = crypto.randomUUID();    const timestamp = new Date().toISOString();
    // Adım 2: CustomerCreated eventi emit et    await this.eventStore.appendEvent({      eventId: crypto.randomUUID(),      customerId,      timestamp,      eventType: 'CustomerCreated',      email: command.email,      firstName: command.firstName,      lastName: command.lastName,      phone: command.phone,      source: command.source,      ipAddress: command.ipAddress,      userAgent: command.userAgent    });
    // Adım 3: Marketing consent verdilerse, ConsentGranted emit et    if (command.marketingConsent) {      await this.eventStore.appendEvent({        eventId: crypto.randomUUID(),        customerId,        timestamp,        eventType: 'ConsentGranted',        purpose: 'marketing',        channel: 'email',        ipAddress: command.ipAddress,        userAgent: command.userAgent,        consentMethod: 'registration-checkbox'      });    }
    // Adım 4: EmailVerificationRequested emit et    await this.eventStore.appendEvent({      eventId: crypto.randomUUID(),      customerId,      timestamp,      eventType: 'EmailVerificationRequested',      email: command.email,      verificationToken: crypto.randomUUID()    });
    return customerId;  }
  private async validateRegistration(    command: CustomerRegistrationCommand  ): Promise<void> {    // Email format validation    if (!this.validator.isValid(command.email)) {      throw new Error('Geçersiz email formatı');    }
    // Müşterinin zaten var olup olmadığını kontrol et    const existing = await this.customerQuery.findByEmail(command.email);    if (existing) {      throw new Error('Müşteri zaten mevcut');    }
    // Şartlar kabul edilmeli    if (!command.termsAccepted) {      throw new Error('Şartlar kabul edilmeli');    }  }}

Bu event'lerden projection oluşturma:

typescript
class CustomerProjectionBuilder {  async handleCustomerCreated(event: CustomerCreated): Promise<void> {    // Read veritabanında ilk müşteri kaydını oluştur    await this.readDB.putItem({      TableName: 'customers',      Item: {        customerId: { S: event.customerId },        email: { S: event.email },        firstName: { S: event.firstName },        lastName: { S: event.lastName },        phone: { S: event.phone || '' },        source: { S: event.source },        status: { S: 'pending-verification' },        createdAt: { S: event.timestamp },        updatedAt: { S: event.timestamp },        // Email lookup'ları için GSI        emailLowercase: { S: event.email.toLowerCase() }      }    });  }
  async handleEmailVerificationRequested(    event: EmailVerificationRequested  ): Promise<void> {    // Verification link ile hoş geldin emaili tetikle    await this.campaignService.triggerCampaign({      campaignId: 'welcome-verification',      customerId: event.customerId,      data: {        verificationToken: event.verificationToken,        email: event.email      }    });  }}

Önemli gotcha: Registration akışının hataları zarif şekilde handle etmesi gerekiyor. Consent eventi yazılamazsa ama müşteri oluşturma başarılıysa, tutarsız state'in oluyor. Atomik multi-event işlemleri için event batching veya saga pattern'leri kullan.

Müşteri Güncelleme İşlemleri

Güncellemeler event sourcing'in parladığı yer - neyin değiştiğinin ve ne zaman değiştiğinin tam geçmişine sahipsin:

typescript
interface UpdateCustomerEmailCommand {  customerId: string;  newEmail: string;  ipAddress: string;  userAgent: string;}
interface UpdateCustomerProfileCommand {  customerId: string;  updates: {    firstName?: string;    lastName?: string;    phone?: string;    dateOfBirth?: string;    address?: Address;  };}
class CustomerUpdateHandler {  async updateEmail(command: UpdateCustomerEmailCommand): Promise<void> {    // Event'lerden mevcut state'i yükle    const events = await this.eventStore.getCustomerEvents(command.customerId);    const customer = this.rehydrateCustomer(events);
    // Business rule: Silinmiş müşteri için email güncellenemez    if (customer.status === 'deleted') {      throw new Error('Silinmiş müşteri güncellenemez');    }
    // Business rule: Email farklı olmalı    if (customer.email === command.newEmail) {      throw new Error('Email değişmedi');    }
    // Email değişim eventi emit et    await this.eventStore.appendEvent({      eventId: crypto.randomUUID(),      customerId: command.customerId,      timestamp: new Date().toISOString(),      eventType: 'CustomerEmailUpdated',      oldEmail: customer.email,      newEmail: command.newEmail,      ipAddress: command.ipAddress,      userAgent: command.userAgent,      requiresVerification: true    });
    // Yeni email için verification tetikle    await this.eventStore.appendEvent({      eventId: crypto.randomUUID(),      customerId: command.customerId,      timestamp: new Date().toISOString(),      eventType: 'EmailVerificationRequested',      email: command.newEmail,      verificationToken: crypto.randomUUID()    });  }
  async updateProfile(command: UpdateCustomerProfileCommand): Promise<void> {    const events = await this.eventStore.getCustomerEvents(command.customerId);    const customer = this.rehydrateCustomer(events);
    if (customer.status === 'deleted') {      throw new Error('Silinmiş müşteri güncellenemez');    }
    // Her güncelleme tipi için spesifik event'ler emit et    const timestamp = new Date().toISOString();
    if (command.updates.firstName || command.updates.lastName) {      await this.eventStore.appendEvent({        eventId: crypto.randomUUID(),        customerId: command.customerId,        timestamp,        eventType: 'CustomerNameUpdated',        oldFirstName: customer.firstName,        oldLastName: customer.lastName,        newFirstName: command.updates.firstName || customer.firstName,        newLastName: command.updates.lastName || customer.lastName      });    }
    if (command.updates.phone) {      await this.eventStore.appendEvent({        eventId: crypto.randomUUID(),        customerId: command.customerId,        timestamp,        eventType: 'CustomerPhoneUpdated',        oldPhone: customer.phone,        newPhone: command.updates.phone      });    }
    if (command.updates.address) {      await this.eventStore.appendEvent({        eventId: crypto.randomUUID(),        customerId: command.customerId,        timestamp,        eventType: 'CustomerAddressUpdated',        oldAddress: customer.address,        newAddress: command.updates.address      });    }  }}

Projection güncellemeleri artımlı değişiklikleri handle eder:

typescript
class CustomerProjectionBuilder {  async handleCustomerEmailUpdated(    event: CustomerEmailUpdated  ): Promise<void> {    await this.readDB.updateItem({      TableName: 'customers',      Key: { customerId: { S: event.customerId } },      UpdateExpression:        'SET email = :newEmail, emailLowercase = :emailLower, ' +        'emailVerified = :verified, updatedAt = :timestamp',      ExpressionAttributeValues: {        ':newEmail': { S: event.newEmail },        ':emailLower': { S: event.newEmail.toLowerCase() },        ':verified': { BOOL: false },        ':timestamp': { S: event.timestamp }      }    });
    // Compliance için audit trail projection    await this.auditDB.putItem({      TableName: 'customer-audit-trail',      Item: {        customerId: { S: event.customerId },        timestamp: { S: event.timestamp },        eventType: { S: 'EmailUpdated' },        oldValue: { S: event.oldEmail },        newValue: { S: event.newEmail },        ipAddress: { S: event.ipAddress },        userAgent: { S: event.userAgent }      }    });  }
  async handleCustomerAddressUpdated(    event: CustomerAddressUpdated  ): Promise<void> {    await this.readDB.updateItem({      TableName: 'customers',      Key: { customerId: { S: event.customerId } },      UpdateExpression: 'SET address = :address, updatedAt = :timestamp',      ExpressionAttributeValues: {        ':address': { S: JSON.stringify(event.newAddress) },        ':timestamp': { S: event.timestamp }      }    });  }}

Müşteri Silme ve Deaktivasyonu

Event sourcing'in geleneksel sistemlerden önemli ölçüde farklılaştığı yer:

typescript
interface DeactivateCustomerCommand {  customerId: string;  reason: 'customer-request' | 'fraud' | 'terms-violation' | 'other';  notes?: string;}
interface DeleteCustomerDataCommand {  customerId: string;  reason: 'gdpr-request' | 'data-retention-policy';  deletionType: 'soft' | 'hard' | 'anonymize';}
class CustomerDeletionHandler {  // Soft delete - müşteri hesabı deaktive edilir ama veri korunur  async deactivateCustomer(    command: DeactivateCustomerCommand  ): Promise<void> {    const events = await this.eventStore.getCustomerEvents(command.customerId);    const customer = this.rehydrateCustomer(events);
    if (customer.status === 'deleted') {      throw new Error('Müşteri zaten silinmiş');    }
    // Deactivation eventi emit et    await this.eventStore.appendEvent({      eventId: crypto.randomUUID(),      customerId: command.customerId,      timestamp: new Date().toISOString(),      eventType: 'CustomerDeactivated',      reason: command.reason,      notes: command.notes,      previousStatus: customer.status    });
    // Otomatik olarak tüm aktif marketing consent'leri iptal et    const activeConsents = customer.consents.filter(c => c.status === 'active');
    for (const consent of activeConsents) {      await this.eventStore.appendEvent({        eventId: crypto.randomUUID(),        customerId: command.customerId,        timestamp: new Date().toISOString(),        eventType: 'ConsentRevoked',        purpose: consent.purpose,        channel: consent.channel,        reason: 'account-deactivated'      });    }  }
  // GDPR silme - deactivation'dan farklı  async deleteCustomerData(    command: DeleteCustomerDataCommand  ): Promise<void> {    const timestamp = new Date().toISOString();
    // Deletion request eventi emit et    await this.eventStore.appendEvent({      eventId: crypto.randomUUID(),      customerId: command.customerId,      timestamp,      eventType: 'CustomerDataDeletionRequested',      reason: command.reason,      deletionType: command.deletionType    });
    if (command.deletionType === 'anonymize') {      // Tüm event'lerde PII'ı anonimleştir      await this.gdprService.anonymizeCustomerEvents(command.customerId);    } else if (command.deletionType === 'hard') {      // Event'leri gerçekten sil (nadir, sadece spesifik yasal gereksinimler için)      await this.gdprService.hardDeleteCustomerEvents(command.customerId);    }
    // Projection'larda silinmiş olarak işaretle    await this.eventStore.appendEvent({      eventId: crypto.randomUUID(),      customerId: command.customerId,      timestamp,      eventType: 'CustomerDataDeleted',      deletionType: command.deletionType,      completedAt: timestamp    });  }}

Aktif kampanyalara etkisi:

typescript
class CampaignService {  async handleCustomerDeactivated(    event: CustomerDeactivated  ): Promise<void> {    // Bu müşteri için zamanlanmış tüm kampanyaları iptal et    const scheduledCampaigns = await this.getScheduledCampaigns(      event.customerId    );
    for (const campaign of scheduledCampaigns) {      await this.cancelCampaign(campaign.id, 'customer-deactivated');    }
    // Tüm segmentlerden çıkar    await this.segmentService.removeFromAllSegments(event.customerId);  }
  async handleCustomerDataDeleted(    event: CustomerDataDeleted  ): Promise<void> {    // Müşteriyi tüm sistemlerden temizle    await this.purgeFromCampaignQueues(event.customerId);    await this.purgeFromSegments(event.customerId);    await this.purgeFromRecommendations(event.customerId);
    // Compliance completion kaydet    await this.complianceLog.recordDeletion({      customerId: event.customerId,      deletionType: event.deletionType,      completedAt: event.timestamp    });  }}

Temel fark: Deactivation geri döndürülebilir ve analytics için veri tutar. GDPR silme kalıcıdır ve tüm sistemlerde ilgili verinin dikkatli handle edilmesini gerektirir.

Event Trigger'ları ile Marketing Otomasyonu

Marketing otomasyonu trigger koşullarını izleyen event processor'lar serisine dönüşüyor:

typescript
interface CampaignTrigger {  triggerId: string;  campaignId: string;  eventPattern: {    eventType: string;    conditions?: Record<string, any>;  };  actions: CampaignAction[];}
interface CampaignAction {  type: 'send-email' | 'send-sms' | 'add-to-segment' | 'wait';  config: any;}
class CampaignOrchestrator {  constructor(    private triggers: CampaignTrigger[],    private consentService: ConsentService,    private channelOrchestrator: ChannelOrchestrator  ) {}
  async handleEvent(event: CustomerEvent): Promise<void> {    // Eşleşen trigger'ları bul    const matchingTriggers = this.triggers.filter(trigger =>      this.eventMatches(event, trigger.eventPattern)    );
    for (const trigger of matchingTriggers) {      await this.executeCampaign(event.customerId, trigger);    }  }
  private async executeCampaign(    customerId: string,    trigger: CampaignTrigger  ): Promise<void> {    // Herhangi bir outbound iletişim öncesi consent kontrol et    const hasConsent = await this.consentService.hasActiveConsent(      customerId,      'marketing',      'email' // Action type'dan türetilecek    );
    if (!hasConsent) {      console.log(`Kampanya ${trigger.campaignId} atlanıyor - consent yok`);      return;    }
    // Action'ları sırayla execute et    for (const action of trigger.actions) {      await this.executeAction(customerId, action, trigger.campaignId);    }  }
  private async executeAction(    customerId: string,    action: CampaignAction,    campaignId: string  ): Promise<void> {    switch (action.type) {      case 'send-email':        await this.channelOrchestrator.sendEmail({          customerId,          campaignId,          templateId: action.config.templateId,          // Duplicate send'leri önlemek için idempotency key          idempotencyKey: `${campaignId}-${customerId}-${Date.now()}`        });        break;
      case 'wait':        // Blocking wait değil scheduled event olarak implement et        await this.scheduleDelayedAction(          customerId,          campaignId,          action.config.duration        );        break;
      case 'add-to-segment':        await this.eventStore.appendEvent({          eventId: crypto.randomUUID(),          customerId,          timestamp: new Date().toISOString(),          eventType: 'CustomerSegmentAdded',          segmentId: action.config.segmentId,          source: `campaign:${campaignId}`        });        break;    }  }
  private eventMatches(    event: CustomerEvent,    pattern: CampaignTrigger['eventPattern']  ): boolean {    if (event.eventType !== pattern.eventType) return false;
    if (!pattern.conditions) return true;
    // Basit condition matching - production JSONPath benzeri kullanır    return Object.entries(pattern.conditions).every(([key, value]) =>      (event as any)[key] === value    );  }}

Gerçek dünya örneği: Terk edilmiş sepet kampanyası

typescript
// Trigger configurationconst abandonedCartTrigger: CampaignTrigger = {  triggerId: 'abandoned-cart-v2',  campaignId: 'abandoned-cart-email',  eventPattern: {    eventType: 'CartAbandoned',    conditions: {      cartValue: { $gte: 50 } // Sadece 50 TL üzeri sepetler için    }  },  actions: [    {      type: 'wait',      config: { duration: '1hour' }    },    {      type: 'send-email',      config: {        templateId: 'abandoned-cart-reminder',        // Dynamic content inject edilecek        personalization: ['cartItems', 'discountCode']      }    },    {      type: 'wait',      config: { duration: '24hours' }    },    {      type: 'send-email',      config: {        templateId: 'abandoned-cart-final-offer',        personalization: ['cartItems', 'largerDiscountCode']      }    }  ]};

Kritik gotcha: Idempotency. Event'ler retry'lar nedeniyle birden fazla işlenebilir. Her action'ın bir idempotency key'e ihtiyacı var:

typescript
class EmailChannelHandler {  private sentMessages = new Set<string>();
  async sendEmail(request: SendEmailRequest): Promise<void> {    // Idempotency key kullanarak zaten gönderilip gönderilmediğini kontrol et    const exists = await this.messageStore.exists(request.idempotencyKey);
    if (exists) {      console.log(`Email zaten gönderildi: ${request.idempotencyKey}`);      return;    }
    // Provider üzerinden gönder    const result = await this.emailProvider.send({      to: request.recipientEmail,      template: request.templateId,      data: request.personalization    });
    // Send event'i kaydet    await this.eventStore.appendEvent({      eventId: crypto.randomUUID(),      customerId: request.customerId,      timestamp: new Date().toISOString(),      eventType: 'EmailSent',      campaignId: request.campaignId,      templateId: request.templateId,      messageId: result.messageId,      idempotencyKey: request.idempotencyKey    });  }}

Kanal Orkestrasyonu ve Tercih Yönetimi

Farklı müşteriler farklı zamanlarda farklı kanallar istiyor. Event-driven mimari tercih yönetimini basit hale getiriyor:

typescript
class ChannelOrchestrator {  constructor(    private preferenceStore: PreferenceProjection,    private channels: Map<string, ChannelHandler>  ) {}
  async determineChannel(    customerId: string,    messageType: string  ): Promise<string[]> {    // Read model'dan müşteri tercihlerini al    const prefs = await this.preferenceStore.getPreferences(customerId);
    // Business logic: Tercihlere ve mesaj tipine göre kanalları seç    const availableChannels: string[] = [];
    if (prefs.emailEnabled && this.shouldUseEmail(messageType, prefs)) {      availableChannels.push('email');    }
    if (prefs.smsEnabled && this.shouldUseSMS(messageType, prefs)) {      availableChannels.push('sms');    }
    if (prefs.pushEnabled && this.shouldUsePush(messageType, prefs)) {      availableChannels.push('push');    }
    // Tercih set edilmemişse fallback stratejisi    if (availableChannels.length === 0) {      return this.getDefaultChannels(messageType);    }
    return availableChannels;  }
  private shouldUseEmail(    messageType: string,    prefs: CustomerPreferences  ): boolean {    // Transactional email'ler her zaman gönderilir    if (messageType === 'transactional') return true;
    // Marketing email'leri frekans tercihine saygı gösterir    if (messageType === 'marketing') {      const lastEmail = prefs.lastEmailSent;      const frequency = prefs.emailFrequency || 'weekly';
      if (!lastEmail) return true;
      const hoursSinceLastEmail =        (Date.now() - new Date(lastEmail).getTime()) / (1000 * 60 * 60);
      switch (frequency) {        case 'daily': return hoursSinceLastEmail >= 24;        case 'weekly': return hoursSinceLastEmail >= 168;        case 'never': return false;        default: return true;      }    }
    return true;  }
  private shouldUseSMS(    messageType: string,    prefs: CustomerPreferences  ): boolean {    // SMS pahalı - dikkatli kullan    // Sadece yüksek değerli transactional veya acil marketing için    return messageType === 'transactional' ||           (messageType === 'urgent-marketing' && prefs.smsForPromotions);  }
  private shouldUsePush(    messageType: string,    prefs: CustomerPreferences  ): boolean {    // Push düşük maliyetli ama ignore edilebilir    // Zamana duyarlı içerik için iyi    const lastPush = prefs.lastPushSent;
    // Spam yapma - saatte en fazla bir push    if (lastPush) {      const hoursSinceLastPush =        (Date.now() - new Date(lastPush).getTime()) / (1000 * 60 * 60);
      if (hoursSinceLastPush < 1) return false;    }
    return prefs.pushCategories?.includes(messageType) ?? false;  }}

Tercih güncellemeleri için event akışı:

Event'ler Aracılığıyla GDPR Uyumluluğu

"Unutulma hakkı" aslında event sourcing ile daha kolay:

typescript
class GDPRComplianceService {  constructor(    private eventStore: EventStore,    private projectionRebuilder: ProjectionRebuilder  ) {}
  async handleDataDeletionRequest(customerId: string): Promise<void> {    // Adım 1: Deletion event emit et    await this.eventStore.appendEvent({      eventId: crypto.randomUUID(),      customerId,      timestamp: new Date().toISOString(),      eventType: 'CustomerDataDeletionRequested',      reason: 'gdpr-right-to-be-forgotten'    });
    // Adım 2: Mevcut event'lerde PII'ı anonimleştir    // Analytics için event'leri tut ama tanımlayıcı veriyi kaldır    const events = await this.eventStore.getCustomerEvents(customerId);
    for (const event of events) {      await this.anonymizeEvent(event);    }
    // Adım 3: Anonimleştirilmiş veri ile projection'ları rebuild et    await this.projectionRebuilder.rebuild(customerId);
    // Adım 4: Audit trail için completion event emit et    await this.eventStore.appendEvent({      eventId: crypto.randomUUID(),      customerId,      timestamp: new Date().toISOString(),      eventType: 'CustomerDataDeleted',      eventsAnonymized: events.length    });  }
  private async anonymizeEvent(event: CustomerEvent): Promise<void> {    // PII'ı anonimleştirilmiş değerlerle değiştir    const anonymized = {      ...event,      email: this.hashPII(event.email || ''),      ipAddress: this.maskIP(event.ipAddress || ''),      userAgent: '[REDACTED]',      // Analytics için non-PII'ı tut      eventType: event.eventType,      timestamp: event.timestamp    };
    await this.eventStore.replaceEvent(event.eventId, anonymized);  }
  private hashPII(value: string): string {    // Anonimleştirme için tek yönlü hash    return crypto.createHash('sha256').update(value).digest('hex');  }
  private maskIP(ip: string): string {    // Genel lokasyonu tut, spesifik identifier'ı kaldır    const parts = ip.split('.');    return `${parts[0]}.${parts[1]}.0.0`;  }}

Önemli düşünce: Gerçek deletion mi yoksa anonimleştirme mi gerektiğine erken karar ver. Analytics ve business intelligence için anonimleştirilmiş event'ler değerli. Compliance için yaklaşımını net bir şekilde belgele.

Satın Alma Akışı ve E-ticaret Event'leri

E-ticaret entegrasyonu event-driven CRM'in gerçek gücünü gösterdiği yer. Göz atmadan teslimat

a kadar her adım marketing otomasyonunu yönlendiren event'ler oluşturuyor.

Sipariş Event Zinciri

Tam bir satın alma zengin bir event stream oluşturuyor:

typescript
// Tam sipariş yaşam döngüsü event'leriinterface CartCreated extends CustomerEvent {  eventType: 'CartCreated';  cartId: string;  sessionId: string;  source: 'web' | 'mobile' | 'api';}
interface ItemAddedToCart extends CustomerEvent {  eventType: 'ItemAddedToCart';  cartId: string;  productId: string;  productName: string;  quantity: number;  price: number;  currency: string;}
interface CartAbandoned extends CustomerEvent {  eventType: 'CartAbandoned';  cartId: string;  items: CartItem[];  totalValue: number;  currency: string;  abandonedAt: string;  timeInCart: number; // saniye}
interface OrderPlaced extends CustomerEvent {  eventType: 'OrderPlaced';  orderId: string;  cartId: string;  items: OrderItem[];  subtotal: number;  tax: number;  shipping: number;  total: number;  currency: string;  shippingAddress: Address;  billingAddress: Address;}
interface PaymentInitiated extends CustomerEvent {  eventType: 'PaymentInitiated';  orderId: string;  paymentMethod: 'credit-card' | 'paypal' | 'bank-transfer';  amount: number;  currency: string;  paymentProvider: string;}
interface PaymentSucceeded extends CustomerEvent {  eventType: 'PaymentSucceeded';  orderId: string;  paymentId: string;  amount: number;  currency: string;  transactionId: string;}
interface PaymentFailed extends CustomerEvent {  eventType: 'PaymentFailed';  orderId: string;  paymentId: string;  amount: number;  errorCode: string;  errorMessage: string;  retryable: boolean;}
interface OrderConfirmed extends CustomerEvent {  eventType: 'OrderConfirmed';  orderId: string;  confirmationNumber: string;  estimatedDelivery: string;}
interface OrderShipped extends CustomerEvent {  eventType: 'OrderShipped';  orderId: string;  trackingNumber: string;  carrier: string;  shippedAt: string;  estimatedDelivery: string;}
interface OrderDelivered extends CustomerEvent {  eventType: 'OrderDelivered';  orderId: string;  deliveredAt: string;  signedBy?: string;}

Event'lerden sipariş aggregate yeniden oluşturma:

typescript
class OrderAggregate {  orderId: string;  customerId: string;  status: OrderStatus;  items: OrderItem[] = [];  totalValue: number = 0;  paymentStatus: PaymentStatus;  shippingStatus: ShippingStatus;  timeline: OrderEvent[] = [];
  static fromEvents(events: CustomerEvent[]): OrderAggregate {    const order = new OrderAggregate();
    for (const event of events) {      order.apply(event);    }
    return order;  }
  private apply(event: CustomerEvent): void {    this.timeline.push(event);
    switch (event.eventType) {      case 'OrderPlaced':        this.orderId = event.orderId;        this.customerId = event.customerId;        this.items = event.items;        this.totalValue = event.total;        this.status = 'pending-payment';        break;
      case 'PaymentSucceeded':        this.paymentStatus = 'paid';        this.status = 'confirmed';        break;
      case 'PaymentFailed':        this.paymentStatus = 'failed';        this.status = 'payment-failed';        break;
      case 'OrderShipped':        this.shippingStatus = 'shipped';        this.status = 'in-transit';        break;
      case 'OrderDelivered':        this.shippingStatus = 'delivered';        this.status = 'completed';        break;    }  }
  // Event geçmişine dayalı business logic  canBeCancelled(): boolean {    return this.status === 'pending-payment' || this.status === 'confirmed';  }
  canBeRefunded(): boolean {    return this.paymentStatus === 'paid' &&           this.shippingStatus !== 'delivered';  }
  getTimeInStatus(status: OrderStatus): number {    const statusEvents = this.timeline.filter(e =>      this.eventResultsInStatus(e, status)    );
    if (statusEvents.length === 0) return 0;
    const startTime = new Date(statusEvents[0].timestamp).getTime();    const endTime = Date.now();    return endTime - startTime;  }}

Ödeme Event Handling'i

Ödeme hataları event-driven sistemlerde özel dikkat gerektiriyor:

typescript
class PaymentEventHandler {  async handlePaymentFailed(event: PaymentFailed): Promise<void> {    // Analytics için hatayı kaydet    await this.analyticsService.trackPaymentFailure({      orderId: event.orderId,      errorCode: event.errorCode,      amount: event.amount    });
    if (event.retryable) {      // Geçici hatalar için otomatik retry zamanla      await this.schedulePaymentRetry(event.orderId, {        attempt: 1,        maxAttempts: 3,        backoffSeconds: 300 // 5 dakika      });    } else {      // Retry edilemeyen hata - recovery kampanyası tetikle      await this.campaignService.triggerCampaign({        campaignId: 'payment-failed-recovery',        customerId: event.customerId,        data: {          orderId: event.orderId,          errorMessage: this.getFriendlyErrorMessage(event.errorCode),          amount: event.amount,          currency: event.currency        }      });    }
    // Sipariş projection'ını güncelle    await this.orderProjection.updatePaymentStatus(      event.orderId,      'failed',      event.errorCode    );  }
  async handlePaymentSucceeded(event: PaymentSucceeded): Promise<void> {    // Sipariş onay workflow'unu tetikle    await this.eventStore.appendEvent({      eventId: crypto.randomUUID(),      customerId: event.customerId,      timestamp: new Date().toISOString(),      eventType: 'OrderConfirmed',      orderId: event.orderId,      confirmationNumber: this.generateConfirmationNumber(),      estimatedDelivery: this.calculateDeliveryDate()    });  }}

İade handling:

typescript
interface RefundInitiated extends CustomerEvent {  eventType: 'RefundInitiated';  orderId: string;  refundId: string;  amount: number;  reason: 'customer-request' | 'quality-issue' | 'delivery-failed' | 'other';  refundType: 'full' | 'partial';  items?: string[]; // Kısmi iadeler için}
interface RefundCompleted extends CustomerEvent {  eventType: 'RefundCompleted';  orderId: string;  refundId: string;  amount: number;  completedAt: string;  transactionId: string;}
class RefundHandler {  async initiateRefund(command: InitiateRefundCommand): Promise<void> {    const orderEvents = await this.eventStore.getOrderEvents(command.orderId);    const order = OrderAggregate.fromEvents(orderEvents);
    // Business rules    if (!order.canBeRefunded()) {      throw new Error('Sipariş iade edilemez');    }
    const refundId = crypto.randomUUID();
    await this.eventStore.appendEvent({      eventId: crypto.randomUUID(),      customerId: order.customerId,      timestamp: new Date().toISOString(),      eventType: 'RefundInitiated',      orderId: command.orderId,      refundId,      amount: command.amount,      reason: command.reason,      refundType: command.refundType    });
    // Ödeme provider ile işle    await this.paymentProvider.processRefund({      transactionId: order.paymentTransactionId,      amount: command.amount    });  }}

Satın Alma Sonrası Marketing Otomasyonu

Sipariş yaşam döngüsü sofistike marketing kampanyalarını yönlendiriyor:

typescript
class PostPurchaseAutomation {  private campaigns: CampaignTrigger[] = [    {      triggerId: 'order-confirmation',      campaignId: 'order-confirmation-email',      eventPattern: {        eventType: 'OrderConfirmed'      },      actions: [        {          type: 'send-email',          config: {            templateId: 'order-confirmation',            personalization: ['orderDetails', 'estimatedDelivery']          }        }      ]    },    {      triggerId: 'shipping-notification',      campaignId: 'shipping-update',      eventPattern: {        eventType: 'OrderShipped'      },      actions: [        {          type: 'send-email',          config: {            templateId: 'order-shipped',            personalization: ['trackingNumber', 'carrier']          }        },        {          type: 'send-push',          config: {            title: 'Siparişin kargoya verildi!',            body: 'Teslimatını takip et'          }        }      ]    },    {      triggerId: 'delivery-review-request',      campaignId: 'post-delivery-review',      eventPattern: {        eventType: 'OrderDelivered'      },      actions: [        {          type: 'wait',          config: { duration: '3days' }        },        {          type: 'send-email',          config: {            templateId: 'review-request',            personalization: ['products', 'reviewLinks']          }        }      ]    },    {      triggerId: 'replenishment-campaign',      campaignId: 'reorder-reminder',      eventPattern: {        eventType: 'OrderDelivered',        conditions: {          // Sadece tüketilebilir ürünler için          productCategory: 'consumables'        }      },      actions: [        {          type: 'wait',          config: { duration: '30days' }        },        {          type: 'send-email',          config: {            templateId: 'reorder-reminder',            personalization: ['products', 'subscriptionOption']          }        }      ]    }  ];
  async handleOrderEvent(event: CustomerEvent): Promise<void> {    const matchingCampaigns = this.campaigns.filter(campaign =>      this.eventMatches(event, campaign.eventPattern)    );
    for (const campaign of matchingCampaigns) {      await this.executeCampaign(event.customerId, campaign, event);    }  }}

Satın Alma Tabanlı Müşteri Segmentasyonu

Event'ler sofistike müşteri segmentasyonu sağlıyor:

typescript
class CustomerSegmentationEngine {  async handleOrderDelivered(event: OrderDelivered): Promise<void> {    // Event geçmişinden müşteri yaşam boyu değerini hesapla    const purchaseEvents = await this.eventStore.getCustomerPurchases(      event.customerId    );
    const ltv = this.calculateLTV(purchaseEvents);    const orderFrequency = this.calculateFrequency(purchaseEvents);    const avgOrderValue = this.calculateAOV(purchaseEvents);
    // Yüksek değerli müşteri tanımlaması    if (ltv > 1000 && orderFrequency > 5) {      await this.eventStore.appendEvent({        eventId: crypto.randomUUID(),        customerId: event.customerId,        timestamp: new Date().toISOString(),        eventType: 'CustomerSegmentAdded',        segmentId: 'high-value-customers',        segmentName: 'Yüksek Değerli Müşteriler',        criteria: { ltv, orderFrequency }      });    }
    // Ürün ilgisi takibi    const productPreferences = this.analyzeProductAffinity(purchaseEvents);    for (const [category, affinity] of Object.entries(productPreferences)) {      if (affinity > 0.7) {        await this.eventStore.appendEvent({          eventId: crypto.randomUUID(),          customerId: event.customerId,          timestamp: new Date().toISOString(),          eventType: 'ProductAffinityDetected',          category,          affinityScore: affinity,          recommendedProducts: this.getRecommendations(category)        });      }    }  }
  // RFM (Recency, Frequency, Monetary) segmentasyonu  async calculateRFMSegments(customerId: string): Promise<RFMSegment> {    const events = await this.eventStore.getCustomerPurchases(customerId);    const now = Date.now();
    // Recency: Son satın almadan bu yana günler    const lastPurchase = events.filter(e => e.eventType === 'OrderDelivered')      .sort((a, b) => b.timestamp.localeCompare(a.timestamp))[0];
    const daysSinceLastPurchase = lastPurchase      ? (now - new Date(lastPurchase.timestamp).getTime()) / (1000 * 60 * 60 * 24)      : 999;
    // Frequency: Satın alma sayısı    const frequency = events.filter(e => e.eventType === 'OrderDelivered').length;
    // Monetary: Toplam harcama    const monetary = events      .filter(e => e.eventType === 'PaymentSucceeded')      .reduce((sum, e) => sum + e.amount, 0);
    // Skorla ve segmentlendir    const rfmScore = {      recency: this.scoreRecency(daysSinceLastPurchase),      frequency: this.scoreFrequency(frequency),      monetary: this.scoreMonetary(monetary)    };
    const segment = this.determineRFMSegment(rfmScore);
    return {      customerId,      recency: daysSinceLastPurchase,      frequency,      monetary,      score: rfmScore,      segment,      calculatedAt: new Date().toISOString()    };  }
  private determineRFMSegment(score: RFMScore): string {    // Şampiyonlar: Yüksek değer, sık, yakın    if (score.recency >= 4 && score.frequency >= 4 && score.monetary >= 4) {      return 'champions';    }
    // Sadık Müşteriler: Sık alıcılar    if (score.frequency >= 4) {      return 'loyal-customers';    }
    // Risk Altında: Eskiden iyiydi, düşüşte    if (score.recency <= 2 && score.frequency >= 3 && score.monetary >= 3) {      return 'at-risk';    }
    // Yeni Müşteriler: Yakın ama düşük frekans    if (score.recency >= 4 && score.frequency <= 2) {      return 'new-customers';    }
    // Dikkat Gerektiriyor: Ortalamanın altında    return 'needs-attention';  }}

Müşteri Yolculuğu ve Huni İzleme

Touchpoint'ler arasında müşteri yolculuklarını izlemek optimizasyon fırsatlarını ortaya çıkarıyor ve kişiselleştirmeyi yönlendiriyor.

Journey Event Tanımları

Kapsamlı yolculuk izleme ince taneli event'ler gerektiriyor:

typescript
// Farkındalık aşaması event'leriinterface PageViewed extends CustomerEvent {  eventType: 'PageViewed';  pageUrl: string;  pageTitle: string;  referrer: string;  sessionId: string;  timeOnPage: number;}
interface ProductViewed extends CustomerEvent {  eventType: 'ProductViewed';  productId: string;  productName: string;  productCategory: string;  price: number;  viewSource: 'search' | 'category' | 'recommendation' | 'direct';}
interface SearchPerformed extends CustomerEvent {  eventType: 'SearchPerformed';  query: string;  resultsCount: number;  selectedResult?: string;  sessionId: string;}
// Değerlendirme aşaması event'leriinterface ProductCompared extends CustomerEvent {  eventType: 'ProductCompared';  productIds: string[];  comparisonAttributes: string[];}
interface ReviewRead extends CustomerEvent {  eventType: 'ReviewRead';  productId: string;  reviewId: string;  rating: number;}
interface VideoWatched extends CustomerEvent {  eventType: 'VideoWatched';  videoId: string;  productId?: string;  watchDuration: number;  totalDuration: number;  completionRate: number;}
// Dönüşüm aşaması event'leriinterface CheckoutStarted extends CustomerEvent {  eventType: 'CheckoutStarted';  cartId: string;  cartValue: number;  itemCount: number;}
interface CheckoutStepCompleted extends CustomerEvent {  eventType: 'CheckoutStepCompleted';  cartId: string;  step: 'shipping' | 'payment' | 'review';  stepNumber: number;}
interface CheckoutAbandoned extends CustomerEvent {  eventType: 'CheckoutAbandoned';  cartId: string;  lastCompletedStep: string;  abandonedValue: number;  timeInCheckout: number;}

Event'lerden Huni Oluşturma

Event stream'lerinden yeniden oluşturulan huni analizi:

typescript
class FunnelAnalyzer {  // Huni aşamalarını tanımla  private readonly purchaseFunnel = [    { stage: 'awareness', events: ['PageViewed', 'ProductViewed'] },    { stage: 'consideration', events: ['ProductCompared', 'ReviewRead'] },    { stage: 'intent', events: ['ItemAddedToCart', 'CartCreated'] },    { stage: 'checkout', events: ['CheckoutStarted', 'CheckoutStepCompleted'] },    { stage: 'purchase', events: ['OrderPlaced', 'PaymentSucceeded'] }  ];
  async analyzeFunnel(    customerId: string,    startDate: string,    endDate: string  ): Promise<FunnelAnalysis> {    const events = await this.eventStore.getCustomerEvents(      customerId,      startDate,      endDate    );
    const funnelProgress: FunnelStage[] = [];    let currentStage = 0;
    for (const event of events) {      const stage = this.getFunnelStage(event.eventType);
      if (stage !== null && stage >= currentStage) {        funnelProgress.push({          stage: this.purchaseFunnel[stage].stage,          event: event.eventType,          timestamp: event.timestamp,          data: event        });        currentStage = Math.max(currentStage, stage + 1);      }    }
    // Drop-off noktalarını belirle    const dropOffPoint = this.identifyDropOff(funnelProgress);
    // Her aşamada harcanan süreyi hesapla    const stageMetrics = this.calculateStageMetrics(funnelProgress);
    return {      customerId,      stages: funnelProgress,      dropOffPoint,      metrics: stageMetrics,      completed: currentStage === this.purchaseFunnel.length,      conversionRate: currentStage / this.purchaseFunnel.length    };  }
  private identifyDropOff(progress: FunnelStage[]): DropOffAnalysis | null {    const lastStage = progress[progress.length - 1];    const expectedNextStage = this.getNextStage(lastStage.stage);
    if (!expectedNextStage) {      return null; // Huni tamamlandı    }
    const timeSinceLastStage =      Date.now() - new Date(lastStage.timestamp).getTime();
    return {      stage: lastStage.stage,      nextExpectedStage: expectedNextStage,      timeSinceLastActivity: timeSinceLastStage,      likelihood: this.calculateDropOffLikelihood(timeSinceLastStage)    };  }
  private calculateStageMetrics(    progress: FunnelStage[]  ): Map<string, StageMetrics> {    const metrics = new Map<string, StageMetrics>();
    for (let i = 0; i < progress.length - 1; i++) {      const current = progress[i];      const next = progress[i + 1];
      const timeInStage =        new Date(next.timestamp).getTime() -        new Date(current.timestamp).getTime();
      metrics.set(current.stage, {        stage: current.stage,        timeSpent: timeInStage,        progressedToNext: true,        events: progress.filter(p => p.stage === current.stage)      });    }
    return metrics;  }}

Çok Dokunuşlu Attribution

Hangi touchpoint'lerin dönüşümleri yönlendirdiğini anlamak:

typescript
interface TouchPoint {  timestamp: string;  channel: 'email' | 'sms' | 'push' | 'web' | 'social' | 'paid-ad';  campaign?: string;  eventType: string;  value?: number;}
class AttributionEngine {  async calculateAttribution(    customerId: string,    conversionEvent: OrderPlaced  ): Promise<AttributionModel> {    // Dönüşüme yol açan tüm touchpoint'leri al    const touchpoints = await this.getCustomerTouchpoints(      customerId,      conversionEvent.timestamp    );
    // Farklı attribution modelleri uygula    return {      firstTouch: this.firstTouchAttribution(touchpoints, conversionEvent),      lastTouch: this.lastTouchAttribution(touchpoints, conversionEvent),      linear: this.linearAttribution(touchpoints, conversionEvent),      timeDecay: this.timeDecayAttribution(touchpoints, conversionEvent),      positionBased: this.positionBasedAttribution(touchpoints, conversionEvent)    };  }
  private firstTouchAttribution(    touchpoints: TouchPoint[],    conversion: OrderPlaced  ): Attribution {    const first = touchpoints[0];    return {      model: 'first-touch',      attribution: {        [first.channel]: {          credit: 100,          value: conversion.total,          campaign: first.campaign        }      }    };  }
  private lastTouchAttribution(    touchpoints: TouchPoint[],    conversion: OrderPlaced  ): Attribution {    const last = touchpoints[touchpoints.length - 1];    return {      model: 'last-touch',      attribution: {        [last.channel]: {          credit: 100,          value: conversion.total,          campaign: last.campaign        }      }    };  }
  private linearAttribution(    touchpoints: TouchPoint[],    conversion: OrderPlaced  ): Attribution {    const creditPerTouch = 100 / touchpoints.length;    const valuePerTouch = conversion.total / touchpoints.length;
    const attribution: Record<string, ChannelAttribution> = {};
    for (const touch of touchpoints) {      if (!attribution[touch.channel]) {        attribution[touch.channel] = {          credit: 0,          value: 0,          touchCount: 0        };      }
      attribution[touch.channel].credit += creditPerTouch;      attribution[touch.channel].value += valuePerTouch;      attribution[touch.channel].touchCount += 1;    }
    return {      model: 'linear',      attribution    };  }
  private timeDecayAttribution(    touchpoints: TouchPoint[],    conversion: OrderPlaced  ): Attribution {    const conversionTime = new Date(conversion.timestamp).getTime();    const halfLife = 7 * 24 * 60 * 60 * 1000; // 7 gün milisaniye cinsinden
    // Exponential decay kullanarak ağırlıkları hesapla    const weights = touchpoints.map(touch => {      const touchTime = new Date(touch.timestamp).getTime();      const age = conversionTime - touchTime;      return Math.exp(-age / halfLife);    });
    const totalWeight = weights.reduce((sum, w) => sum + w, 0);
    const attribution: Record<string, ChannelAttribution> = {};
    touchpoints.forEach((touch, i) => {      const credit = (weights[i] / totalWeight) * 100;      const value = (weights[i] / totalWeight) * conversion.total;
      if (!attribution[touch.channel]) {        attribution[touch.channel] = { credit: 0, value: 0, touchCount: 0 };      }
      attribution[touch.channel].credit += credit;      attribution[touch.channel].value += value;      attribution[touch.channel].touchCount += 1;    });
    return {      model: 'time-decay',      attribution    };  }
  private positionBasedAttribution(    touchpoints: TouchPoint[],    conversion: OrderPlaced  ): Attribution {    // İlk dokunuşa %40, son dokunuşa %40, ortaya %20 dağıtılır    const attribution: Record<string, ChannelAttribution> = {};
    if (touchpoints.length === 1) {      return this.firstTouchAttribution(touchpoints, conversion);    }
    const first = touchpoints[0];    const last = touchpoints[touchpoints.length - 1];    const middle = touchpoints.slice(1, -1);
    // İlk dokunuş: %40    attribution[first.channel] = {      credit: 40,      value: conversion.total * 0.4,      touchCount: 1    };
    // Son dokunuş: %40    if (!attribution[last.channel]) {      attribution[last.channel] = { credit: 0, value: 0, touchCount: 0 };    }    attribution[last.channel].credit += 40;    attribution[last.channel].value += conversion.total * 0.4;    attribution[last.channel].touchCount += 1;
    // Orta dokunuşlar: %20 eşit dağıtılır    if (middle.length > 0) {      const creditPerMiddle = 20 / middle.length;      const valuePerMiddle = (conversion.total * 0.2) / middle.length;
      for (const touch of middle) {        if (!attribution[touch.channel]) {          attribution[touch.channel] = { credit: 0, value: 0, touchCount: 0 };        }        attribution[touch.channel].credit += creditPerMiddle;        attribution[touch.channel].value += valuePerMiddle;        attribution[touch.channel].touchCount += 1;      }    }
    return {      model: 'position-based',      attribution    };  }}

Gerçek Zamanlı Huni İlerleme Kampanyaları

Huni pozisyonuna dayalı kampanya tetikleme:

typescript
class FunnelProgressionAutomation {  private funnelCampaigns: FunnelCampaign[] = [    {      name: 'Terk Edilmiş Göz Atma Kurtarma',      trigger: {        stage: 'awareness',        inactivityMinutes: 30,        condition: 'viewed-multiple-products-no-cart'      },      actions: [        {          type: 'send-email',          config: {            templateId: 'browse-abandonment',            personalization: ['viewedProducts', 'recommendations']          }        }      ]    },    {      name: 'Checkout Terki',      trigger: {        stage: 'checkout',        inactivityMinutes: 60,        condition: 'started-checkout-not-completed'      },      actions: [        {          type: 'wait',          config: { duration: '1hour' }        },        {          type: 'send-email',          config: {            templateId: 'checkout-abandonment',            personalization: ['cartItems', 'checkoutLink', 'incentive']          }        },        {          type: 'wait',          config: { duration: '24hours' }        },        {          type: 'send-sms',          config: {            message: 'Siparişini tamamla ve %10 indirim kazan!'          }        }      ]    },    {      name: 'Satın Alma Sonrası Çapraz Satış',      trigger: {        stage: 'purchase',        condition: 'order-delivered'      },      actions: [        {          type: 'wait',          config: { duration: '7days' }        },        {          type: 'send-email',          config: {            templateId: 'cross-sell',            personalization: ['purchasedProducts', 'recommendations']          }        }      ]    }  ];
  async monitorFunnelProgress(): Promise<void> {    // Huni aşamalarında takılı kalan müşterileri kontrol etmek için periyodik çalışır    const stuckCustomers = await this.findStuckCustomers();
    for (const customer of stuckCustomers) {      const analysis = await this.funnelAnalyzer.analyzeFunnel(        customer.customerId,        customer.sessionStart,        new Date().toISOString()      );
      const matchingCampaigns = this.funnelCampaigns.filter(campaign =>        this.shouldTriggerCampaign(campaign, analysis)      );
      for (const campaign of matchingCampaigns) {        await this.executeFunnelCampaign(customer.customerId, campaign, analysis);      }    }  }
  private shouldTriggerCampaign(    campaign: FunnelCampaign,    analysis: FunnelAnalysis  ): boolean {    if (!analysis.dropOffPoint) return false;
    const dropOff = analysis.dropOffPoint;    const inactivityMinutes = dropOff.timeSinceLastActivity / (1000 * 60);
    return (      dropOff.stage === campaign.trigger.stage &&      inactivityMinutes >= campaign.trigger.inactivityMinutes &&      this.checkCondition(campaign.trigger.condition, analysis)    );  }}

Bu kapsamlı yolculuk izleme ve huni analizi hassas, veri odaklı marketing kararlarını mümkün kılıyor. Event'lerden müşteri yollarını yeniden oluşturarak, müşterilerin tam olarak nerede zorlandıklarını belirleyebilir ve hedefli kampanyalarla müdahale edebilirsin.

Entegrasyon Pattern'leri

Üçüncü Parti Marketing Tool'ları ile Bağlantı

Çoğu marketing ekibi SendGrid, Mailchimp veya HubSpot gibi özel tool'lar kullanıyor. Event-driven faydaları korurken nasıl entegre edeceğin:

typescript
interface MarketingIntegration {  syncCustomer(customerId: string, data: CustomerData): Promise<void>;  syncConsent(customerId: string, consent: ConsentData): Promise<void>;  syncSegment(customerId: string, segments: string[]): Promise<void>;}
class SendGridIntegration implements MarketingIntegration {  constructor(    private apiKey: string,    private eventStore: EventStore  ) {}
  async handleCustomerEvent(event: CustomerEvent): Promise<void> {    switch (event.eventType) {      case 'CustomerCreated':        await this.syncCustomer(event.customerId, {          email: event.email,          created_at: event.timestamp        });        break;
      case 'ConsentGranted':        if (event.purpose === 'marketing' && event.channel === 'email') {          await this.syncConsent(event.customerId, {            status: 'subscribed',            timestamp: event.timestamp          });        }        break;
      case 'ConsentRevoked':        if (event.purpose === 'marketing' && event.channel === 'email') {          await this.syncConsent(event.customerId, {            status: 'unsubscribed',            timestamp: event.timestamp          });        }        break;
      case 'CustomerSegmentAdded':        await this.addToList(event.customerId, event.segmentId);        break;    }
    // Debug için integration event kaydet    await this.eventStore.appendEvent({      eventId: crypto.randomUUID(),      customerId: event.customerId,      timestamp: new Date().toISOString(),      eventType: 'ThirdPartyIntegrationSynced',      integration: 'sendgrid',      action: event.eventType    });  }
  async syncCustomer(customerId: string, data: CustomerData): Promise<void> {    // Retry logic ile SendGrid API call    await this.sendGridAPI.post('/marketing/contacts', {      contacts: [{        email: data.email,        custom_fields: {          customer_id: customerId,          created_at: data.created_at        }      }]    });  }
  // Diğer metodları implement et...}

Başarısız İletişimler için Dead Letter Queue'lar

Tüm mesajlar başarıyla deliver edilmez. Event-driven mimari failure handling'i açık hale getirir:

typescript
class ChannelHandler {  constructor(    private provider: EmailProvider,    private eventStore: EventStore,    private dlqHandler: DeadLetterQueueHandler  ) {}
  async sendMessage(    customerId: string,    message: OutboundMessage  ): Promise<void> {    try {      const result = await this.provider.send(message);
      // Başarıyı kaydet      await this.eventStore.appendEvent({        eventId: crypto.randomUUID(),        customerId,        timestamp: new Date().toISOString(),        eventType: 'MessageSent',        channel: 'email',        messageId: result.messageId,        campaignId: message.campaignId      });
    } catch (error) {      // Error retry edilebilir mi kontrol et      if (this.isRetryable(error)) {        throw error; // Event bus retry etsin      }
      // Retry edilemeyen error - DLQ'ya gönder      await this.dlqHandler.handleFailedMessage({        customerId,        message,        error: error.message,        timestamp: new Date().toISOString()      });
      // Failure event kaydet      await this.eventStore.appendEvent({        eventId: crypto.randomUUID(),        customerId,        timestamp: new Date().toISOString(),        eventType: 'MessageFailed',        channel: 'email',        campaignId: message.campaignId,        errorType: error.code,        errorMessage: error.message      });    }  }
  private isRetryable(error: any): boolean {    // Provider rate limit'leri, network sorunları - retry    const retryableCodes = ['RATE_LIMIT', 'TIMEOUT', 'SERVICE_UNAVAILABLE'];    return retryableCodes.includes(error.code);  }}
class DeadLetterQueueHandler {  async handleFailedMessage(failure: FailedMessage): Promise<void> {    // Manuel review için DLQ'da sakla    await this.dlqStore.save(failure);
    // Yüksek failure rate'te on-call'a alert at    const recentFailures = await this.getRecentFailures('1hour');    if (recentFailures.length > 100) {      await this.alerting.trigger({        severity: 'high',        message: `Yüksek email failure rate: Son saatte ${recentFailures.length}`,        failures: recentFailures.slice(0, 10)      });    }
    // Spesifik errorler için otomatik aksiyon al    if (failure.error.includes('invalid-email')) {      // Müşteri kaydında email'i geçersiz olarak işaretle      await this.eventStore.appendEvent({        eventId: crypto.randomUUID(),        customerId: failure.customerId,        timestamp: new Date().toISOString(),        eventType: 'CustomerEmailInvalidated',        reason: 'bounced-permanent'      });    }  }}

Ölçeklendirme Düşünceleri ve Trade-off'lar

Performans: Gerçek Zamanlı vs Batch

Ekiplerin şu kararla zorlandığını gördüm: projection'lar gerçek zamanlı mı yoksa batch'lerde mi güncellenmeli?

Gerçek zamanlı işleme:

  • Artıları: Müşteri değişiklikleri anında görür, marketing kampanyaları daha hızlı tepki verir
  • Eksileri: Daha yüksek maliyetler, daha karmaşık altyapı, potansiyel thundering herd
  • En iyisi: Consent güncellemeleri, transactional bildirimler

Batch işleme:

  • Artıları: Daha iyi throughput, optimize etmesi daha kolay, daha ucuz
  • Eksileri: Eventual consistency gecikmesi, query'lerde stale data
  • En iyisi: Analytics projection'ları, segment hesaplamaları, günlük email kampanyaları

İyi çalışan hibrit bir yaklaşım:

typescript
class ProjectionOrchestrator {  // Kritik projection'lar anında güncellenir  private realTimeProjections = new Set(['consent', 'preferences']);
  // Analytics projection'ları her 5 dakikada batch  private batchProjections = new Set(['customer-360', 'segments']);
  async handleEvent(event: CustomerEvent): Promise<void> {    const projectionType = this.classifyProjection(event.eventType);
    if (this.realTimeProjections.has(projectionType)) {      // Anında işle      await this.updateProjection(projectionType, event);    } else {      // Batch queue'ya ekle      await this.batchQueue.enqueue(projectionType, event);    }  }
  private classifyProjection(eventType: string): string {    // Event'leri projection type'larına map et    const mapping: Record<string, string> = {      'ConsentGranted': 'consent',      'ConsentRevoked': 'consent',      'PreferencesUpdated': 'preferences',      'CustomerSegmentAdded': 'segments',      'ProductViewed': 'customer-360'    };
    return mapping[eventType] || 'customer-360';  }}

Maliyet Optimizasyonu

Dikkatli olmazsan event-driven CRM hızla pahalılaşabilir. Öğrendiklerim:

Event storage maliyetleri write volume ile ölçekleniyor. TTL'leri agresif kullan:

typescript
// Detaylı event'leri 90 gün tut, sonra aggregate etconst eventRetentionPolicy = {  detailed: 90 * 86400, // Saniye cinsinden 90 gün  aggregated: 7 * 365 * 86400 // Compliance için 7 yıl};
class EventArchiver {  async archiveOldEvents(): Promise<void> {    const cutoffDate = new Date();    cutoffDate.setDate(cutoffDate.getDate() - 90);
    // Eski event'leri olan müşterileri al    const customersToArchive = await this.getCustomersWithOldEvents(cutoffDate);
    for (const customerId of customersToArchive) {      const events = await this.eventStore.getCustomerEvents(        customerId,        undefined,        cutoffDate.toISOString()      );
      // Aggregate edilmiş özet oluştur      const summary = this.aggregateEvents(events);      await this.summaryStore.save(customerId, summary);
      // Detaylı event'leri sil (TTL var ama cleanup garantiler)      await this.eventStore.deleteEvents(        customerId,        cutoffDate.toISOString()      );    }  }
  private aggregateEvents(events: CustomerEvent[]): EventSummary {    return {      totalEvents: events.length,      eventTypes: this.countByType(events),      firstEvent: events[0]?.timestamp,      lastEvent: events[events.length - 1]?.timestamp,      consentHistory: this.summarizeConsents(events),      // Yasal olarak gerekli veriyi tut      gdprAuditTrail: this.buildAuditTrail(events)    };  }}

Lambda maliyetleri event processor'lar için - mümkün olduğunda batch:

typescript
// Her event'i ayrı ayrı işlemek yerine// Micro-batch'lerde işleclass BatchedEventProcessor {  private buffer: CustomerEvent[] = [];  private flushInterval = 5000; // 5 saniye  private maxBatchSize = 100;
  constructor() {    setInterval(() => this.flush(), this.flushInterval);  }
  async addEvent(event: CustomerEvent): Promise<void> {    this.buffer.push(event);
    if (this.buffer.length >= this.maxBatchSize) {      await this.flush();    }  }
  private async flush(): Promise<void> {    if (this.buffer.length === 0) return;
    const batch = this.buffer.splice(0, this.maxBatchSize);
    // Tüm batch'i tek Lambda invocation'da işle    await this.projectionBuilder.processBatch(batch);  }}

Schema Evolution Stratejisi

Event schema'ların değişecek. Bunun için plan yap:

typescript
interface EventSchema {  version: number;  schema: any;  compatibleWith?: number[];}
class SchemaRegistry {  private schemas = new Map<string, EventSchema[]>();
  registerSchema(eventType: string, schema: EventSchema): void {    const existing = this.schemas.get(eventType) || [];    existing.push(schema);    this.schemas.set(eventType, existing);  }
  getLatestSchema(eventType: string): EventSchema | undefined {    const schemas = this.schemas.get(eventType);    return schemas?.[schemas.length - 1];  }
  // Saklamadan önce event'i schema'ya göre validate et  async validateEvent(event: CustomerEvent): Promise<boolean> {    const schema = this.getLatestSchema(event.eventType);    if (!schema) {      console.warn(`${event.eventType} için kayıtlı schema yok`);      return false;    }
    // Validation için JSON Schema veya benzeri kullan    return this.validator.validate(event, schema.schema);  }}

Öğrendiklerim ve Gotcha'lar

Birkaç event-driven CRM sistemi implement ettikten sonra, sürekli önemli olan pattern'ler:

1. Idempotency Pazarlık Götürmez

Her external action (email send, API call, database write) idempotent olmalı. Event'ler replay edilecek, processor'lar retry edecek ve bunu handle etmezsen duplicate email göndereceksin.

Kullandığım pattern: her action ile idempotency key'leri sakla ve execute etmeden önce kontrol et.

Consent kontrolü her mesaj gönderisine 200ms eklerse bottleneck'in olur. Consent durumunu agresif şekilde cache'le, 5-10 dakika TTL ile. Marketing email'leri için bu gecikme kabul edilebilir. Transactional email'ler için daha kısa TTL veya gerçek zamanlı kontrollere ihtiyacın olabilir.

3. Event Sıralaması Düşündüğünden Daha Az Önemli

Çoğu ekip event ordering konusunda endişeleniyor ama CRM için nadiren kritik. Müşteri tercihleri iki kez hızlı bir şekilde güncellerse final state önemli olan. Conflict'leri handle etmek için timestamp'lar ve version numaraları kullan:

typescript
class ConflictResolution {  mergePreferences(existing: Preferences, incoming: Preferences): Preferences {    // Timestamp'a göre last-write-wins    return {      emailFrequency:        incoming.updatedAt > existing.emailFrequency.updatedAt          ? incoming.emailFrequency          : existing.emailFrequency,      categories:        incoming.updatedAt > existing.categories.updatedAt          ? incoming.categories          : existing.categories    };  }}

4. Basit Başla, Gerektiğinde Karmaşıklık Ekle

Basit "kayıt sonrası email gönder" akışları için karmaşık saga orkestratörleri kuran ekipler gördüm. Temel event handler'lar ile başla. Saga pattern'lerini sadece compensation logic'li çok adımlı workflow'ların olduğunda ekle.

5. Monitoring Farklı

Geleneksel CRM monitoring "veritabanı ayakta mı?" kontrol eder. Event-driven monitoring kontrol eder:

  • Event processing lag (projection'lar ne kadar geride?)
  • Dead letter queue depth (kaç failure?)
  • Projection consistency (aggregate event replay ile eşleşiyor mu?)
typescript
class CRMHealthCheck {  async checkHealth(): Promise<HealthStatus> {    const checks = await Promise.all([      this.checkEventProcessingLag(),      this.checkDLQDepth(),      this.checkProjectionConsistency()    ]);
    return {      status: checks.every(c => c.healthy) ? 'healthy' : 'degraded',      checks    };  }
  private async checkEventProcessingLag(): Promise<HealthCheck> {    const latestEvent = await this.eventStore.getLatestEvent();    const latestProjection = await this.projectionStore.getLatestUpdate();
    const lagMs = new Date(latestEvent.timestamp).getTime() -                  new Date(latestProjection.timestamp).getTime();
    return {      name: 'event-processing-lag',      healthy: lagMs < 60000, // 1 dakikadan az      value: lagMs,      message: `Projection lag: ${lagMs}ms`    };  }}

Kapanış Düşünceleri

Event-driven CRM mimarisi gerçek problemleri çözüyor: GDPR uyumluluğu, çok kanallı orkestrasyon ve gerçek zamanlı kişiselleştirme. Ama yeni karmaşıklık getiriyor: eventual consistency, event schema evolution ve daha fazla moving part.

Pattern en iyi şunlar gerektiğinde çalışıyor:

  • Compliance için tam audit trail'ler
  • Karmaşık, çok adımlı marketing otomasyonu
  • Birçok external sistemle entegrasyon
  • Tek veritabanı limitlerinin ötesinde ölçeklenebilirlik

Şunlar varken overkill:

  • Basit email list yönetimi
  • Küçük müşteri tabanı (< 100k)
  • Öncelikle transactional iletişimler

Consent ve tercihler için event sourcing ile başla - bu tam commitment olmadan GDPR uyumluluk faydaları sağlar. Read pattern'lerin write pattern'lerden önemli ölçüde ayrıştığında CQRS ekle. Sofistike workflow'lara ihtiyacın olduğunda marketing otomasyonunu event'ler üzerine inşa et.

Mimari güçlü CRM yetenekleri sağlıyor ama her pattern gibi spesifik problemler için bir araç. Değer kattığı yerlerde kullan, ilginç olduğu için değil.

İlgili Yazılar