Em arquiteturas de microsserviços, o email frequentemente atravessa vários serviços. Veja como projetar sistemas de email que funcionam bem com arquiteturas distribuídas.
Limites de serviço
Serviço de email dedicado
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ User Svc │ │ Order Svc │ │ Billing Svc │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
│ Events │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────┐
│ Message Bus │
└─────────────────────────────────────────────────────────┘
│
▼
┌──────────────┐
│ Email Svc │
└──────────────┘
Email orientado a 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
}
});
}
Padrões de comunicação entre serviços
Assíncrono via fila de mensagens
// 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);
}
});
Síncrono via API (quando necessário)
// 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'
}
});
}
}
Propriedade de dados
Acesso a dados do usuário
// 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']
});
}
Propriedade de templates
// 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;
};
}
Tratamento de falhas
Retentativas com 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 });
}
}
}
Idempotência
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;
}
Transações entre serviços
Padrão 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'
}
};
Observabilidade
Rastreamento distribuído
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();
}
}
Boas práticas
- —Orientado a eventos por padrão - Baixo acoplamento entre serviços
- —Operações idempotentes - Trate eventos duplicados com elegância
- —Propriedade clara - Quem é responsável por templates, dados de usuário, preferências
- —Trace tudo - IDs de correlação entre serviços
- —Degradação graciosa - Falhas de email não devem impedir a conclusão de pedidos
- —Centralize a lógica de email - Um serviço, comportamento consistente
Email em microsserviços é sobre limites bem definidos e comunicação confiável. Projete pensando em falhas e você construirá algo robusto.