İçeriğe atla

2025-10-26

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 müşteri durumunu doğrudan saklar: müşteri başına bir kayıt, her etkileşimde yerinde güncellenir. Bu model; ürünün aynı anda gerçek zamanlı kişiselleştirme, GDPR uyumlu denetim izi ve çok kanallı orkestrasyon gereksinimi duymasıyla çöker, çünkü güncel-durum satırının oraya nasıl geldiğine dair bir hafızası yoktur. Event-driven CRM bu modeli tersine çevirir: her etkileşim değişmez bir event olarak kaydedilir ve herhangi bir görüntü (profil, consent defteri, kanal geçmişi) bu event akışı üzerinde bir projeksiyon olur. Bu yazı, AWS üzerinde event-driven bir CRM’in mimarisini, kişiselleştirme ve consent için projeksiyonları, kanallar arası orkestrasyon katmanını ve bu desenin getirdiği dengeleri (depolama maliyeti, eventual-consistency bütçeleri, replay pencereleri) ele alır.

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:

Delivery Channels

Domain Services

Query Side - CQRS Read

Command Side - CQRS Write

Event Infrastructure

Customer Touchpoints

Check Consent

Check Preferences

Web App

Mobile App

Email Client

Support System

API Gateway

Event Bus

EventBridge/Kafka

Event Store

DynamoDB/EventStoreDB

Command Handlers

Business Rules

Event Emitters

Read Models

Customer 360

Consent DB

Preferences DB

Event Processors

Consent Service

Campaign Service

Channel Orchestrator

Email Provider

SendGrid/SES

SMS Provider

Twilio

Push Service

FCM/APNs

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:

// Sadece müşteri oluşturma ve consent ile başla
const 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:

// Basit DynamoDB event store
const 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:

// Her aggregate için bir handler
class 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’ı:

// Müşteri query'leri için tek projection
class 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:

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:

// Mevcut email provider'ını wrap et
class 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:

EmailChannelCampaignEngineProjectionsEventBusEventStoreCommandHandlerAPICustomerEmailChannelCampaignEngineProjectionsEventBusEventStoreCommandHandlerAPICustomerCustomer RegistrationProduct BrowsingCart AbandonmentCustomer Leaves SitePurchase FlowPOST /registerRegisterCustomerCommandCustomerCreated EventPublish EventCustomerCreatedCreate Customer RecordCustomerCreatedSend Welcome EmailEmailSent EventWelcome Email DeliveredGET /products/123ProductViewed EventPublish EventUpdate Customer ActivityPOST /cart/addItemAddedToCart EventPublish EventDetect InactivityWait 1 HourCartAbandoned EventSend ReminderCart Reminder EmailPOST /ordersPlaceOrderCommandOrderPlaced EventPublish EventPaymentInitiated EventProcess PaymentPaymentSucceeded EventPublish EventPaymentSucceededOrderConfirmed EventSend ConfirmationOrder Confirmation EmailUpdate Order StatusCustomer Becomes Buyer

Bu akış için tam kod:

// 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ünceller
async 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ı Tetiklenir
async 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ı İzlenir
async 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ş Verme
async 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:

Retention

Fulfillment

Purchase

Activation

Acquisition

Yes

No

Success

Failed

Lifecycle Management

PreferencesUpdated

SegmentationRecalculated

ConsentRevoked

CampaignsPaused

CustomerDeactivated

ConsentRevoked

CustomerCreated

EmailVerificationRequested

EmailVerified

ConsentGranted

ProductViewed

ItemAddedToCart

Conversion?

OrderPlaced

CartAbandoned

ReminderSent

PaymentInitiated

Payment?

PaymentSucceeded

PaymentFailed

RetryScheduled

OrderConfirmed

OrderShipped

OrderDelivered

ReviewRequested

ReplenishmentCampaign

ReviewSubmitted

ReorderPlaced

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:

// 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:

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:

interface EventUpcaster {
  fromVersion: number;
  toVersion: number;
  upcast(event: any): any;
}

// Örnek: Consent event'lerine GDPR context ekleme
const 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ş:

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ş:

// Projection builder - event bus'tan async çalışır
class 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:

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:

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:

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:

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:

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:

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:

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ı

// Trigger configuration
const 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:

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:

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ışı:

CampaignEngineReadDBPreferenceProjectorEventBusCommandHandlerAPICustomerCampaignEngineReadDBPreferenceProjectorEventBusCommandHandlerAPICustomerMay pause or reschedulepending messagesUpdate Email Frequency to WeeklyUpdatePreferencesCommandPreferencesUpdated EventEvent NotificationUpdate Materialized ViewEvent NotificationRecalculate Active Campaigns

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

“Unutulma hakkı” aslında event sourcing ile daha kolay:

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:

// Tam sipariş yaşam döngüsü event'leri
interface 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:

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:

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:

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:

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:

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:

// Farkındalık aşaması event'leri
interface 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'leri
interface 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'leri
interface 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:

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:

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:

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:

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:

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:

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:

// Detaylı event'leri 90 gün tut, sonra aggregate et
const 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:

// Her event'i ayrı ayrı işlemek yerine
// Micro-batch'lerde işle
class 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:

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:

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?)
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

Harici Yetkilendirme Yönetim Sistemleri: Mimarınız İçin Doğru Platformu Seçmek

AWS Verified Permissions, SpiceDB, OpenFGA, Cerbos ve OPA dahil harici yetkilendirme platformlarının tarafsız değerlendirmesi. Mimari desenler, maliyet analizi ve mühendislik ekipleri için karar çerçevesi.

authorizationsecurityarchitecture+5
Multi-Account AWS Mimarisi: Ölçeklenebilir Event-Driven Sistemler

Dayanıklı event-driven sistemler için multi-account AWS mimari pattern'lerini öğrenin. Hesap yapısı, EventBridge routing, servisler arası iletişim ve dağıtık sistemlerde operasyonel zorlukları keşfedin.

awseventbridgemulti-account+5
SaaS Yetkilendirme için AWS Cognito + Verified Permissions

AWS Cognito ve Verified Permissions ile SaaS yetkilendirme mimarisi. Cedar politika dili, çok kiracılı desenler, JWT token akışı, maliyet analizi ve TypeScript örnekleriyle yaygın hatalar.

authorizationawscognito+4
SpiceDB vs Auth0 FGA: İlişki Tabanlı Yetkilendirme Karşılaştırması

SpiceDB ve Auth0 FGA (OpenFGA) arasında detaylı bir teknik karşılaştırma -- şema tasarımı, tutarlılık modelleri, dağıtım ve ölçeklenebilirlik açısından farklı tercihler yapan iki Zanzibar tabanlı yetkilendirme sistemi.

authorizationsecurityarchitecture+3
TypeScript AI SDK Karşılaştırması: Agent Geliştirme için Vercel AI SDK vs OpenAI Agents SDK

AI agent geliştirmek için TypeScript SDK'larının pratik karşılaştırması - Vercel AI SDK, OpenAI Agents SDK ve AWS Bedrock entegrasyonu. Kod örnekleri, karar frameworkleri ve production patternleri içeriyor.

typescriptai-toolsserverless+4