In Microservices-Architekturen erstreckt sich E‑Mail häufig über mehrere Services. So entwirft man E‑Mail‑Systeme, die in verteilten Architekturen gut funktionieren.
Service-Grenzen
Dedizierter E‑Mail‑Service
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ User Svc │ │ Order Svc │ │ Billing Svc │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
│ Events │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────┐
│ Message Bus │
└─────────────────────────────────────────────────────────┘
│
▼
┌──────────────┐
│ Email Svc │
└──────────────┘
Ereignisgetriebene E‑Mail
// Other services emit events
interface EmailTriggerEvent {
type: string;
userId: string;
data: Record<string, any>;
timestamp: Date;
correlationId: string;
}
// Email service subscribes to relevant events
const emailTriggers = {
'user.created': 'welcome',
'user.password_reset_requested': 'password-reset',
'order.confirmed': 'order-confirmation',
'order.shipped': 'shipping-notification',
'payment.failed': 'payment-failed'
};
async function handleEvent(event: EmailTriggerEvent) {
const templateId = emailTriggers[event.type];
if (!templateId) return;
const user = await userService.getUser(event.userId);
await sendEmail({
to: user.email,
template: templateId,
data: event.data,
metadata: {
correlationId: event.correlationId,
sourceEvent: event.type
}
});
}
Muster für die Service-Kommunikation
Asynchron über Message Queue
// Order service publishes event
await messageQueue.publish('orders', {
type: 'order.confirmed',
orderId: order.id,
userId: order.userId,
data: {
orderNumber: order.number,
items: order.items,
total: order.total
}
});
// Email service consumes
messageQueue.subscribe('orders', async (message) => {
if (message.type === 'order.confirmed') {
await emailService.sendOrderConfirmation(message);
}
});
Synchron über API (bei Bedarf)
// For critical, time-sensitive emails
class EmailClient {
async sendImmediate(email: Email): Promise<SendResult> {
// Direct API call to email service
return fetch(`${EMAIL_SERVICE_URL}/send`, {
method: 'POST',
body: JSON.stringify(email),
headers: {
'Content-Type': 'application/json',
'X-Priority': 'critical'
}
});
}
}
Datenhoheit
Zugriff auf Benutzerdaten
// Email service needs user data but doesn't own it
interface EmailUserData {
email: string;
name: string;
locale: string;
timezone: string;
preferences: NotificationPreferences;
}
// Option 1: Include in event
const event = {
type: 'order.confirmed',
user: { email, name, locale }, // Denormalized
order: { ... }
};
// Option 2: Fetch when needed
async function getUserForEmail(userId: string): Promise<EmailUserData> {
return userServiceClient.getUser(userId, {
fields: ['email', 'name', 'locale', 'timezone', 'preferences']
});
}
Vorlagenhoheit
// Templates can be owned by email service or content service
interface TemplateSource {
// Email service owns transactional templates
transactional: EmailService;
// Marketing service owns campaign templates
marketing: MarketingService;
// Product teams own feature-specific templates
features: {
billing: BillingService;
onboarding: OnboardingService;
};
}
Umgang mit Fehlern
Retry mit Dead-Letter-Queue
async function processEmailEvent(event: EmailTriggerEvent) {
try {
await sendEmail(event);
await ack(event);
} catch (error) {
if (event.attempts < MAX_RETRIES) {
await retry(event, { delay: exponentialBackoff(event.attempts) });
} else {
await deadLetterQueue.push(event);
await alertOps('Email failed after retries', { event, error });
}
}
}
Idempotenz
async function sendEmail(event: EmailTriggerEvent) {
// Check if already sent
const sent = await redis.get(`email:sent:${event.correlationId}`);
if (sent) {
return { status: 'already_sent', id: sent };
}
const result = await emailProvider.send(event);
// Mark as sent
await redis.set(`email:sent:${event.correlationId}`, result.id, 'EX', 86400);
return result;
}
Serviceübergreifende Transaktionen
Saga-Muster für E‑Mail
// Order saga with email step
const orderSaga = {
steps: [
{ service: 'inventory', action: 'reserve' },
{ service: 'payment', action: 'charge' },
{ service: 'order', action: 'confirm' },
{ service: 'email', action: 'sendConfirmation' }
],
compensations: {
'email.sendConfirmation': null, // Can't unsend
'order.confirm': 'order.cancel',
'payment.charge': 'payment.refund',
'inventory.reserve': 'inventory.release'
}
};
Beobachtbarkeit
Verteiltes Tracing
async function sendEmail(event: EmailTriggerEvent) {
const span = tracer.startSpan('email.send', {
parent: extractContext(event.headers),
attributes: {
'email.template': event.templateId,
'email.recipient': hashEmail(event.to),
'event.type': event.type,
'correlation.id': event.correlationId
}
});
try {
const result = await emailProvider.send(event);
span.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (error) {
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
throw error;
} finally {
span.end();
}
}
Bewährte Praktiken
- —Standardmäßig ereignisgetrieben – lose Kopplung zwischen Services
- —Idempotente Operationen – doppelte Events elegant handhaben
- —Klare Zuständigkeiten – wer besitzt Vorlagen, Benutzerdaten, Präferenzen
- —Alles tracen – Korrelations‑IDs über alle Services hinweg
- —Robuste Degradation – E‑Mail‑Fehler sollten Bestellungen nicht zum Scheitern bringen
- —E‑Mail‑Logik zentralisieren – ein Service, konsistentes Verhalten
E‑Mail in Microservices dreht sich um klare Grenzen und zuverlässige Kommunikation. Plane für Ausfälle und du baust etwas Robustes.