Development

Building Event-Driven Email Notifications with Kafka in a Timesheet System

By Ginbok4 min read

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:

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

  1. Use dedicated topics: Separate Kafka topics for different message types (mail, SMS, push notifications)
  2. Implement retry logic: Handle transient failures with exponential backoff
  3. Monitor queue depth: Alert when message backlogs exceed thresholds
  4. Version your messages: Use versioned message schemas for backward compatibility
  5. 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.

#asp.net-core#architecture#backend#automation#workflow#Timesheet#integration#kafka#event-driven-architecture
← Back to Articles