emailr_
Tous les articles
usecase·10 min

E-mail dans les microservices : modèles de conception

microservicesarchitecturepatterns

Dans les architectures microservices, l’e-mail traverse souvent plusieurs services. Voici comment concevoir des systèmes e-mail qui s’intègrent bien aux architectures distribuées.

Limites de service

Service e-mail dédié

┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│  User Svc    │     │  Order Svc   │     │ Billing Svc  │
└──────┬───────┘     └──────┬───────┘     └──────┬───────┘
       │                    │                    │
       │    Events          │                    │
       ▼                    ▼                    ▼
┌─────────────────────────────────────────────────────────┐
│                    Message Bus                          │
└─────────────────────────────────────────────────────────┘
                           │
                           ▼
                  ┌──────────────┐
                  │  Email Svc   │
                  └──────────────┘

E-mail piloté par les événements

// 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
    }
  });
}

Modèles de communication entre services

Asynchrone via une file de messages

// 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);
  }
});

Synchrone via API (si nécessaire)

// 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'
      }
    });
  }
}

Propriété des données

Accès aux données utilisateur

// 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']
  });
}

Propriété des modèles

// 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;
  };
}

Gestion des échecs

Relance avec file de lettres mortes

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 });
    }
  }
}

Idempotence

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;
}

Transactions inter-services

Modèle Saga pour l’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'
  }
};

Observabilité

Traçage distribué

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();
  }
}

Bonnes pratiques

  1. Orienté événements par défaut - Faible couplage entre services
  2. Opérations idempotentes - Gérer les événements dupliqués en douceur
  3. Propriété claire - Qui possède les modèles, les données utilisateur, les préférences
  4. Tracer tout - Identifiants de corrélation à travers les services
  5. Dégradation maîtrisée - Les échecs d’e-mail ne doivent pas faire échouer les commandes
  6. Centraliser la logique e-mail - Un seul service, comportement cohérent

L’e-mail dans les microservices, c’est des limites nettes et une communication fiable. Concevez en anticipant les défaillances et vous bâtirez quelque chose de robuste.

e_

Écrit par l'équipe emailr

Nous construisons l'infrastructure email pour les développeurs

Prêt à commencer à envoyer ?

Obtenez votre clé API et envoyez votre premier email en moins de 5 minutes. Aucune carte de crédit requise.