emailr_
Todos los artículos
usecase·10 min

Email en microservicios: Patrones de diseño

microservicesarchitecturepatterns

En arquitecturas de microservicios, el email a menudo abarca varios servicios. Aquí tienes cómo diseñar sistemas de email que funcionen bien con arquitecturas distribuidas.

Límites de servicio

Servicio de email dedicado

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

Email basado en eventos

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

Patrones de comunicación entre servicios

Asíncrono mediante cola de mensajes

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

Sincrónico vía API (cuando sea necesario)

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

Propiedad de los datos

Acceso a los datos de usuario

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

Propiedad de las plantillas

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

Gestión de fallos

Reintentos con cola de mensajes muertos

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

Idempotencia

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

Transacciones entre servicios

Patrón Saga para email

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

Observabilidad

Trazabilidad distribuida

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

Mejores prácticas

  1. Basado en eventos por defecto - Acoplamiento débil entre servicios
  2. Operaciones idempotentes - Gestiona los eventos duplicados sin problemas
  3. Propiedad clara - Quién es dueño de las plantillas, los datos de usuario, las preferencias
  4. Traza todo - IDs de correlación a través de los servicios
  5. Degradación elegante - Los fallos de email no deberían bloquear los pedidos
  6. Centraliza la lógica de email - Un solo servicio, comportamiento consistente

El email en microservicios va de límites claros y comunicación fiable. Diseña pensando en los fallos y construirás algo robusto.

e_

Escrito por el equipo de emailr

Construyendo infraestructura de email para desarrolladores

¿Listo para empezar a enviar?

Obtén tu clave API y envía tu primer email en menos de 5 minutos. No se requiere tarjeta de crédito.