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:
- Audit Gereksinimleri: GDPR, consent’in tam olarak ne zaman ve hangi amaçla verildiğini bilmeyi zorunlu kılıyor
- Çok Kanallı Karmaşıklık: Müşteriler email, SMS, push, in-app üzerinden etkileşime giriyor ve her kanalın farklı kuralları var
- Gerçek Zamanlı Kişiselleştirme: Marketing otomasyonunun müşteri davranışına anında tepki vermesi gerekiyor
- Veri Gizliliği: “Unutulma hakkı” event’leri redaction ile replay edebildiğinde daha kolay
- 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:
// 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:
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:
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ışı:
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.
2. Consent Kontrolleri Hızlı Olmalı
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
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.
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.
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.
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.
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.