Saturday, September 26, 2026

Building a Scalable, Production-Grade Notification Service with Spring Boot & Kafka


Modern microservices architectures require a centralized, reliable, and fault-tolerant notification engine. Whether you are sending transactional receipts via email, time-sensitive authentication codes via SMS, or promotional alerts via push notifications, offloading these operations to an asynchronous, event-driven notification service is critical for maintainability and performance.

In this guide, we break down the architecture of a Scalable Notification Service using Spring Boot, Apache Kafka, PostgreSQL, and Third-Party Providers.

1. High-Level Architecture Overview

[ Clients / Services ]
        │
        ▼
[ Spring Cloud Gateway ] ────────► [ JWT / OAuth2 Auth ]
        │
        ▼
[ Notification Service (Spring Boot) ]
  ├── Template Engine
  ├── Preference Service
  ├── Notification Processor
  ├── Retry & Scheduler
  └── Delivery Tracker
        │
        ├───► [ PostgreSQL (Data Store) ]
        ├───► [ Prometheus + Grafana / ELK Stack (Observability) ]
        │
        ▼
[ Kafka Message Queue ]
  ├── notification-email
  ├── notification-sms
  └── notification-push
        │
        ├──────────────────────┬──────────────────────┐
        ▼                      ▼                      ▼
 [ Email Service ]      [ SMS Service ]        [ Push Service ]
 (SendGrid / SMTP)      (Twilio / Nexmo)       (FCM / APNs)

Core Architecture Flow

  1. API Gateway: Clients and internal microservices send notification requests to a unified entry point managed by Spring Cloud Gateway.

  2. Authentication: The gateway validates requests using JWT / OAuth2 tokens.

  3. Core Processing Engine: The Spring Boot notification engine formats messages using a Template Engine, validates User Preferences, registers delivery states in PostgreSQL, and publishes event payloads to Apache Kafka.

  4. Event-Driven Messaging Queue: Kafka decouples core orchestration from delivery logic, distributing messages into channel-specific topics (notification-email, notification-sms, notification-push).

  5. Channel Consumers: Dedicated consumer services subscribe to Kafka topics and send notifications through third-party gateways (e.g., SendGrid, Twilio, FCM).

  6. Observability Stack: Metrics and logs are pushed to Prometheus, Grafana, and an ELK Stack (Elasticsearch, Logstash, Kibana).

2. Deep Dive into Service Components

A. API Gateway & Security

  • Recommended SaaS/PaaS Options:

    • API Gateway: AWS API Gateway, Cloudflare API Shield, or Kong Konnect.

    • Authentication: Auth0, Clerk, or AWS Cognito.

  • Technology: Spring Cloud Gateway, Spring Security JWT / OAuth2.

  • Component Responsibilities:

    • Validates incoming JWT tokens against an identity provider's JWKS (JSON Web Key Set) endpoint.

    • Enforces rate limiting, IP throttling, and DDoS protection before requests hit downstream services.

  • Integration Strategy:

    • Cloudflare or AWS API Gateway sits in front of the application.

    • When a caller makes a request, the Gateway intercepts the Authorization: Bearer <token> header, verifies the signature against Auth0/Clerk without touching the core database, injects sanitized headers (such as X-User-Id), and routes the request to the Notification Engine.

Spring Cloud Gateway Routing Configuration

spring: cloud: gateway: routes: - id: notification-service uri: lb://NOTIFICATION-SERVICE predicates: - Path=/api/v1/notifications/** filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 100 redis-rate-limiter.burstCapacity: 200

B. Core Notification Engine (Spring Boot)

The core engine contains five essential internal modules:

  1. Template Engine (Thymeleaf / Freemarker): Dynamic variable substitution (e.g., replacing ${firstName} in HTML/text templates).

  2. Preference Service: Checks user-configured settings (e.g., user disabled SMS marketing alerts) before rendering or queuing.

  3. Notification Processor: Validates payloads, coordinates components, and writes initialnotification records to PostgreSQL.

  4. Retry & Scheduler: Handles delayed messages, scheduled campaigns, and Dead Letter Queue (DLQ) processing.

  5. Delivery Tracker: Tracks and updates delivery lifecycle statuses (PENDING, QUEUED, DELIVERED, FAILED).

  • Recommended SaaS/PaaS Options:

    • Compute Platform: AWS Fargate (ECS), Google Cloud Run, or Render.

    • Distributed Workflow & Retry Engine: Temporal Cloud or Upstash QStash (for scheduled/delayed jobs).

  • Component Responsibilities:

    • Preference Service & Template Engine: Compiles templates dynamically and verifies user opt-in statuses.

    • Retry Scheduler: Manages complex state retries, backoff strategies, and scheduled future sends (e.g., promotional campaigns).

  • Integration Strategy:

    • Deploy Spring Boot apps as stateless containers on Cloud Run or AWS Fargate configured with auto-scaling rules based on CPU and request concurrency.

    • For scheduled or retried notifications, pass the payload to Temporal Cloud or Upstash QStash. Rather than writing custom database polling algorithms in Spring Boot, Temporal guarantees execution state persistence and handles retry timers across service failures natively.

Main Request DTO & Controller

@RestController
@RequestMapping("/api/v1/notifications")
@RequiredArgsConstructor
public class NotificationController {

    private final NotificationProcessor notificationProcessor;

    @PostMapping("/send")
    public ResponseEntity<NotificationResponse> sendNotification(@Valid @RequestBody NotificationRequest request) {
        String notificationId = notificationProcessor.processAndDispatch(request);
        return ResponseEntity.accepted().body(new NotificationResponse(notificationId, "QUEUED"));
    }
}

public record NotificationRequest(
    @NotBlank String userId,
    @NotNull ChannelType channel, // EMAIL, SMS, PUSH
    @NotBlank String templateId,
    Map<String, Object> templateData
) {}

C. Persistent Storage: PostgreSQL Data Model

PostgreSQL stores persistent state, audit trails, and user preferences.

  • Recommended SaaS/PaaS Options:

    • Serverless PostgreSQL: Neon, Supabase, or AWS Aurora Serverless v2.

    • Caching Layer: Upstash Redis or AWS ElastiCache.

  • Component Responsibilities:

    • Persists user preference maps, template configurations, delivery attempt audit logs, and provider statuses.

    • Caches high-volume user preference checks using Redis to keep lookup latency under 5ms.

  • Integration Strategy:

    • Connect Spring Boot using standard JDBC with connection pooling (HikariCP) directed to Neon or AWS Aurora.

    • Use Upstash Redis as a read-through cache for user preferences (user:{id}:preferences). When preferences update, invalidate or write-through the Redis cache.

CREATE TYPE channel_enum AS ENUM ('EMAIL', 'SMS', 'PUSH');
CREATE TYPE status_enum AS ENUM ('PENDING', 'QUEUED', 'DELIVERED', 'FAILED');

CREATE TABLE notification_logs (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id VARCHAR(64) NOT NULL,
    channel channel_enum NOT NULL,
    template_id VARCHAR(64) NOT NULL,
    status status_enum NOT NULL DEFAULT 'PENDING',
    retry_count INT DEFAULT 0,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE user_preferences (
    user_id VARCHAR(64) PRIMARY KEY,
    email_enabled BOOLEAN DEFAULT TRUE,
    sms_enabled BOOLEAN DEFAULT TRUE,
    push_enabled BOOLEAN DEFAULT TRUE
);

YAML
# Spring Boot application.yml setup for Managed PostgreSQL & Redis
spring:
  datasource:
    url: jdbc:postgresql://ep-example-pooler.eastus2.aws.neon.tech/neondb?sslmode=require
    username: ${DB_USER}
    password: ${DB_PASSWORD}
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
  data:
    redis:
      host: ${UPSTASH_REDIS_HOST}
      port: 6379
      password: ${UPSTASH_REDIS_PASSWORD}
      ssl: true

D. Kafka Event Queue Integration

Kafka serves as the high-throughput, async backbone for distributing execution across consumer clusters.

Kafka Publisher Service Implementation

@Service
@RequiredArgsConstructor
public class KafkaNotificationProducer {

    private final KafkaTemplate<String, NotificationEvent> kafkaTemplate;

    public void publishEvent(NotificationEvent event) {
        String topic = "notification-" + event.getChannel().name().toLowerCase();
        
        kafkaTemplate.send(topic, event.getUserId(), event)
            .whenComplete((result, ex) -> {
                if (ex != null) {
                    // Handle failure to publish to Kafka
                    log.error("Failed to publish notification event to Kafka: {}", ex.getMessage());
                } else {
                    log.info("Event successfully sent to topic {} partition {}", 
                            result.getRecordMetadata().topic(), 
                            result.getRecordMetadata().partition());
                }
            });
    }
}

E. Channel Consumer Services

Each downstream service (Email, SMS, Push) operates as an independent, stateless consumer cluster listening to its corresponding Kafka topic.

[Core Engine] ──► [ Confluent Cloud / Upstash Kafka ] ──► [ Channel Consumers]
                            │
                            └──► [ Dead-Letter Queue (DLQ) ]
  • Recommended SaaS/PaaS Options:

    • Managed Kafka: Confluent Cloud, Upstash Kafka (Serverless), or AWS MSK (Managed Streaming for Apache Kafka).

    • Alternative Lightweight Queues: AWS SQS or RabbitMQ Cloud (CloudAMQP).

  • Component Responsibilities:

    • Buffers inbound notification events during peak traffic spikes.

    • Decouples the ingestion engine from third-party vendor latencies and outages.

    • Routes events to channel-specific partitions (notification-email, notification-sms, notification-push).

  • Integration Strategy:

    • Spring Boot uses Spring Kafka connecting via SASL_SSL to Confluent Cloud or Upstash Kafka.

    • Partition events by userId to preserve event ordering per recipient while enabling parallel consumption across multiple consumer instances

Email Consumer Component (Twilio SendGrid Example)

@Component
@Slf4j
@RequiredArgsConstructor
public class EmailNotificationConsumer {

    private final SendGrid sendGridClient;
    private final DeliveryTracker deliveryTracker;

    @KafkaListener(topics = "notification-email", groupId = "email-consumer-group")
    public void consumeEmailEvent(NotificationEvent event, Acknowledgment ack) {
        try {
            SendGridMail email = buildEmail(event);
            Response response = sendGridClient.api(email);

            if (response.getStatusCode() >= 200 && response.getStatusCode() < 300) {
                deliveryTracker.updateStatus(event.getNotificationId(), "DELIVERED");
                ack.acknowledge(); // Commit offset manually
            } else {
                throw new DeliveryException("SendGrid returned status " + response.getStatusCode());
            }
        } catch (Exception e) {
            log.error("Failed to send email for notification ID {}", event.getNotificationId(), e);
            throw new RuntimeException(e); // Trigger retry / DLQ logic
        }
    }
}

Properties 

# Spring Kafka Confluent Cloud Integration Properties
spring.kafka.bootstrap-servers=${CONFLUENT_BOOTSTRAP_SERVERS}
spring.kafka.properties.security.protocol=SASL_SSL
spring.kafka.properties.sasl.mechanism=PLAIN
spring.kafka.properties.sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required username='${CONFLUENT_API_KEY}' password='${CONFLUENT_API_SECRET}';
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer

3. Delivery Channels (Third-Party SaaS APIs)

A. Email Services

  • Recommended SaaS Options: Resend, AWS SES, SendGrid, or Postmark.

  • Role: Delivers transactional and promotional emails; provides webhooks for status tracking (delivered, bounced, opened).

  • Integration: Direct HTTP REST integration or SMTP protocol. Resend and Postmark offer higher deliverability and faster REST APIs compared to standard SMTP.

B. SMS & Telephony Services

  • Recommended SaaS Options: Twilio, AWS SNS, MessageBird (Bird), or Sinch.

  • Role: Sends SMS codes, WhatsApp messages, and voice alerts globally.

  • Integration: REST APIs triggered inside Kafka SMS consumers, configured with automatic fallback routes to alternative providers (e.g., failover from Twilio to Sinch).

C. Mobile & Web Push Notifications

  • Recommended SaaS Options: Firebase Cloud Messaging (FCM), Apple Push Notification service (APNs), or OneSignal.

  • Role: Manages push token registries and sends web/mobile notifications to iOS, Android, and web browsers.

  • Integration: OneSignal or FCM HTTP v1 API used within push consumers.

                  ┌──► Resend / AWS SES (Email)
                  │
[ Channel ] ──────┼──► Twilio / Sinch (SMS)
 Consumers        │
                  └──► FCM / OneSignal (Push)
                          │
                          ▼ (Status Webhooks)
               [ Notification Webhook Controller ]

3. Failure Scenarios and Resiliency Design

Building a reliable notification service requires proactive handling of failures across every tier.

Failure Scenario

Impact

Mitigation Strategy

Implementation Details

Kafka Broker Down

The Notification Processor cannot publish incoming events.

Transactional Outbox Pattern

Write notification events to PostgreSQL inside the local DB transaction. Use a background scheduler (e.g., Debezium or Spring @Scheduled) to poll and publish to Kafka once available.

Third-Party API Outage (e.g., SendGrid/Twilio Down)

Consumers fail to deliver notifications.

Retry Queue + Circuit Breaker

Implement Resilience4j Circuit Breakers. If error thresholds are breached, route traffic to a secondary failover provider (e.g., AWS SES for email).

Duplicate Message Deliveries

Consumers process the same event twice due to network re-balances or retries.

Idempotent Consumers

Store processed message IDs in Redis or PostgreSQL. Before processing, run SETNX notification:idempotency:{id} to discard duplicate events.

Poison Pill Messages

Malformed payloads crash consumer instances continuously.

Dead Letter Queue (DLQ)

Configure Kafka Spring Listener with DefaultErrorHandler and exponential backoff. After $N$ retries, route the message to notification-email-dlq for manual inspection

Failure Handling Code: Kafka Retry and DLQ Configuration

@Configuration
public class KafkaErrorConfig {

    @Bean
    public CommonErrorHandler errorHandler(KafkaTemplate<Object, Object> template) {
        // Send to DLQ after 3 retries with 2-second interval backoff
        DeadLetterPublishingRecoverer recoverer = new DeadLetterPublishingRecoverer(template);
        FixedBackOff backOff = new FixedBackOff(2000L, 3L);
        return new DefaultErrorHandler(recoverer, backOff);
    }
}

4. Observability, logging & Monitoring

To maintain SLAs across thousands of notifications per second, monitor these metrics using Prometheus, Grafana, and the ELK stack:

  • End-to-End Latency: Time taken from HTTP ingestion to vendor delivery acknowledgment.

  • Consumer Lag: Monitors how far behind Kafka consumers are from the producer log end offset.

  • Delivery Success/Failure Rate: Tracked per channel (email, sms, push) via Prometheus counters.

  • Distributed Tracing: Implemented using Spring Cloud Sleuth / OpenTelemetry to pass traceId through HTTP requests, Kafka headers, and provider logs.

[ Application Logs ] ─────► [ Better Stack / Datadog Logs ]
[ Metrics (Prometheus) ] ──► [ Grafana Cloud / Datadog ]
[ Tracing (OpenTelemetry)]► [ Honeycomb / New Relic ]
  • Recommended SaaS Options:

    • Full-Stack Observability: Datadog, Grafana Cloud, or New Relic.

    • Log Aggregation: Better Stack (Logtail) or AWS CloudWatch.

    • Error Tracking: Sentry.

  • Component Responsibilities:

    • Tracks delivery latencies, API failure rates, queue backlog depths, and code exceptions across all distributed components.

  • Integration Strategy:

    • Embed OpenTelemetry or Micrometer in Spring Boot to export traces and metrics to Grafana Cloud or Datadog over OTLP (OpenTelemetry Protocol).

    • Integrate Sentry SDK to capture unhandled consumer exceptions instantly alongside full stack traces and context variables.

7. Managed Alternative: All-in-One Notification Infrastructure SaaS

If building, maintaining, and scaling custom template engines, user preference matrices, queue pipelines, and multi-provider fallbacks creates unnecessary engineering overhead, use an All-in-One Notification Infrastructure SaaS Platform:

  • Top SaaS Platforms: Novu (Open-source platform), Knock, Courier, or SuprSend.

  • Why Choose This Approach:

    • Replaces custom template engines, preference databases, queuing logic, and vendor failover systems with a single unified API.

    • Provides pre-built UI components for user preference centers and in-app notification centers.

    • Connects directly to underlying providers (SendGrid, Twilio, FCM, Resend) using your existing accounts.

                               ┌──────────────────────────────────────────────┐
                               │           Notification SaaS Platform         │
                               │        (Knock / Novu / Courier / SuprSend)   │
                               │                                              │
[ Backend ] ──► REST / SDK ──► │  ├── Workflow Builder & Template Engine      │ ──► Sends via SendGrid/Twilio/FCM
                               │  ├── User Preference Management              │
                               │  └── Batching, Throttling & Routing Rules     │
                               └──────────────────────────────────────────────┘

Java

// Example: Triggering a complex multi-channel workflow via Knock API
KnockClient knock = new KnockClient("sk_test_12345");

WorkflowTriggerRequest request = WorkflowTriggerRequest.builder()
    .key("welcome-sequence")
    .actor("user_123")
    .recipients(List.of("user_456"))
    .data(Map.of(
        "company_name", "Acme Inc",
        "action_url", "https://app.acme.com/login"
    ))
    .build();

knock.workflows().trigger(request);

Recommended SaaS/PaaS Tech Stack Summary

Component

In-House Build (PaaS Components)

Fully Managed SaaS Alternative

API Gateway & Auth

AWS API Gateway + Auth0 / Clerk

Cloudflare API Shield + Clerk, Tyk

Compute / Orchestration

AWS Fargate / Google Cloud Run

Temporal Cloud (Workflow orchestration)

Data Store & Cache

Neon (Postgres) + Upstash Redis, AWS Aurora

Supabase (Database + Auth)

Message Queue

Confluent Cloud / Upstash Kafka / AWS MKS (kafka stream)

AWS SQS / Redpanda Cloud

Email Channel

Resend / AWS SES

SendGrid / Postmark

SMS Channel

Twilio / Sinch

AWS SNS

Push Channel

Firebase Cloud Messaging (FCM)

OneSignal

Observability

Grafana Cloud + Sentry

Datadog

All-in-One Alternative

N/A (Built from above)

Novu / Knock / Courier

The "Just Buy It" Alternative

Building the architecture above is a fantastic engineering exercise, and for massive scale (think Uber or Netflix), it is necessary. But if your core product isn't a notification service, building user preference centers, maintaining drag-and-drop template editors for product managers, and writing retry logic for five different telecom APIs is a massive distraction.

Notification Infrastructure as a Service (SaaS) has matured rapidly. Platforms like Novu, Knock, and Courier replace the entire middle layer of this architecture.

Instead of deploying Kafka and Postgres, you trigger a workflow via a single REST call. These platforms handle the template rendering, user preference matrices, batching, and intelligent routing (e.g., "Send an in-app ping; if unread for 10 minutes, send an email; if critical, send an SMS").

Java

// Example: Bypassing the custom build with Knock
KnockClient knock = new KnockClient("sk_test_...");

WorkflowTriggerRequest request = WorkflowTriggerRequest.builder()
    .key("onboarding-sequence")
    .actor("system")
    .recipients(List.of("user_8891"))
    .data(Map.of("first_name", "Alex"))
    .build();

knock.workflows().trigger(request);

Building internally gives you ultimate control and infinite horizontal scale. Buying a platform like Knock or Novu gives your engineering team months of their lives back. Which route makes the most sense for the current stage of your platform?

Monday, September 22, 2025

GCP Cloud Quiz - quiz2 Question

Google cloud platform Quiz

☁️ Google cloud Platform

Professional Certification Quiz - 50 Questions

Question 1 of 50 Score: 0/50

Building a Scalable, Production-Grade Notification Service with Spring Boot & Kafka

Modern microservices architectures require a centralized, reliable, and fault-tolerant notification engine. Whether you are sending transact...