Introduction
Modern timesheet applications require reliable email notifications for approvals, submissions, and status updates. Traditional synchronous email sending can slow down your application and create poor user experiences. This article explores implementing an event-driven email notification system using Apache Kafka in an ASP.NET Core timesheet application, ensuring scalability, reliability, and separation of concerns.
Why Event-Driven Email Notifications?
When a user submits their timesheet, they expect an immediate response. However, sending emails synchronously can take several seconds, blocking the request thread and degrading performance. Event-driven architecture solves this by:
- Decoupling operations: Timesheet submission and email sending become independent
- Improving response times: Users get instant feedback while emails process asynchronously
- Ensuring reliability: Failed email attempts don't crash the main application
- Enabling scalability: Email processing scales independently from core business logic
Architecture Overview
Our timesheet system implements a three-tier notification flow:
1. Domain Event Layer
When business events occur (timesheet submission, approval, rejection), the application raises domain events using MediatR's notification pattern. These events capture what happened without knowing how to handle it.
// Domain event raised after timesheet submission
public class TimesheetSubmittedEvent : INotification
{
public Guid TimesheetId { get; set; }
public Guid SubmitterId { get; set; }
public Guid ApproverId { get; set; }
public DateTime SubmittedAt { get; set; }
}
2. Event Handler Layer
Event handlers listen for domain events and transform them into Kafka messages. This layer determines who receives emails, creates notification records, and prepares messages for the queue.
// Event handler converts domain event to Kafka message
public class TimesheetSubmittedHandler : INotificationHandler<TimesheetSubmittedEvent>
{
public async Task Handle(TimesheetSubmittedEvent notification)
{
var mailMessage = new MailMessageV1
{
To = GetApproverEmail(notification.ApproverId),
Subject = "New Timesheet Awaiting Approval",
Body = GenerateEmailBody(notification)
};
_kafkaMessages.MailMessages.Add(new KafkaMessage<MailMessageV1>
{
NotificationId = notificationId,
Value = mailMessage
});
}
}
3. Kafka Pipeline Layer
The KafkaPipeline acts as a post-processor that runs after the main request completes successfully. It pushes all queued messages to Kafka topics, where a separate Mail Consumer Service picks them up and sends actual emails via SMTP (SendGrid, MailGun, etc.).
// Pipeline pushes messages to Kafka after request completion
public class KafkaPipeline<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next)
{
var response = await next();
try
{
foreach (var message in _kafkaMessages.MailMessages)
{
await _producers.MailProducer.ProduceRetryAsync(
AppSettings.Kafka.MailTopic,
message
);
// Mark notification as queued
notification.IsSentToQueue = true;
}
}
catch (Exception e)
{
_logger.LogError(e, "Failed to send to Kafka");
}
return response;
}
}
Key Benefits
Asynchronous Processing: Users don't wait for email delivery. Timesheet submission returns immediately while emails process in the background.
Fault Tolerance: If Kafka is temporarily unavailable, the main application continues functioning. Failed messages can be retried automatically.
Audit Trail: Every notification attempt is logged in the database with IsSentToQueue flags, providing complete visibility into email delivery status.
Environment Safety: Development environments can run without sending emails by simply omitting Kafka configuration. Connection failures are caught gracefully, preventing local testing accidents.
Common Patterns
Multiple Recipients: Project managers and approvers all receive notifications based on project configuration and approval workflows.
Authorization-Based Routing: Emails route to different approvers based on project hierarchy: primary approver, or fallback to project manager.
Best Practices
- Use dedicated topics: Separate Kafka topics for different message types (mail, SMS, push notifications)
- Implement retry logic: Handle transient failures with exponential backoff
- Monitor queue depth: Alert when message backlogs exceed thresholds
- Version your messages: Use versioned message schemas for backward compatibility
- Log everything: Maintain detailed logs of event flow from domain event to email delivery
Conclusion
Event-driven email notifications with Kafka transform timesheet applications from blocking, synchronous operations to responsive, scalable systems. By separating concerns between business logic, event handling, and message delivery, you gain reliability, performance, and maintainability. The pattern extends beyond emails to any asynchronous operation—SMS, push notifications, webhooks, or third-party integrations.
As your timesheet system grows, this architecture scales horizontally by adding consumer instances without touching core application code. It's a proven pattern for enterprise applications requiring robust, high-volume notification delivery.