A scalable, idempotent, and event-driven Newsletter Scheduling and Subscription Service built with Spring Boot, supporting bulk operations, Kafka-based dispatch, multi-threaded scheduling, and horizontal scalability.
The service allows:
- Creating Topics with specified names
- Subscribing users to topics (single or bulk)
- Scheduling content for future dispatch
- Multi-threaded and distributed message delivery
- Kafka integration for asynchronous event processing
- Cron & FixedRate schedulers for recurring dispatch checks
- Requirements
- Architecture & Infrastructure
- Design Overview
- Data Model & Entities
- API Reference
- Service Layer
- Thread Safety & Concurrency
- AWS Deployment
- Application Properties
- How to Run
- Dispatch Flow
| Component | Version / Recommendation |
|---|---|
| Java | 17+ |
| Maven | 3.8+ |
| Spring Boot | 3.x |
| Database | PostgreSQL / MySQL / In Memory |
| Message Broker | Apache Kafka / In Memory |
| Scheduler | Spring Task Scheduler / Cron |
| Cloud | AWS EC2 |
| Tools | Postman, cURL |
The service follows a modular, event-driven architecture designed for scalability and resilience.
- REST APIs → For content & subscriber management.
- Scheduler → For periodic content dispatch.
- Kafka Producers/Consumers → For asynchronous message delivery.
- Thread Pool Executor → For parallel processing of subscribers.
- Database → Persistence of topics, subscribers, and content.
- AWS EC2 → Stateless horizontal scaling.
| Type | Annotation | Use |
|---|---|---|
| Cron Scheduler | @Scheduled(cron = "...") |
For periodic content dispatch every minute/hour |
| Fixed Rate | @Scheduled(fixedRate = 30000) |
Default fallback scheduler every 30s |
- newsletter-events → Published when content is ready to be sent.
| Concern | Approach |
|---|---|
| Idempotency | (email, topic_id) unique constraint in DB + deduplication in bulk requests |
| Bulk Processing | Uses saveAll() for efficient inserts; deduplicates repeated entries in input |
| Indexing | Composite index created on (status, scheduled_time) which provides range queries in O(log n + k) times instead of full table scan |
| Thread Safety | No shared mutable state; relies on database & stateless services |
| Distributed Safety | Enforced at DB-level and through application-level checks |
| Error Handling | Structured JSON responses with "status" keys |
| Scalability | Stateless REST APIs, async jobs, and Kafka-based decoupling |
| Observability | Logs every dispatch event and error per subscriber |
@Entity
public class Topic {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}@Entity
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"email", "topic_id"}))
public class Subscriber {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String email;
@ManyToOne(fetch = FetchType.LAZY)
private Topic topic;
}@Entity
public class Content {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String text;
private LocalDateTime scheduledTime;
@Enumerated(EnumType.STRING)
private Status status = Status.PENDING;
@ManyToOne(fetch = FetchType.LAZY)
private Topic topic;
}public enum Status {
PENDING, SENT, FAILED
}Composite index (status, scheduled_time) ensures that range query is O(log N) instead of full table scan
POST /api/topics
Request
{
"name": "Tech News"
}Response
{
"id": 1,
"name": "Tech News"
}POST /api/subscribers
Request
{
"email": "nitish.joshi1995@gmail.com",
"topicId": 1
}Response
{
"email": "nitish.joshi1995@gmail.com",
"topicId": 1,
"status": "SUBSCRIBED"
}Error Cases
| Condition | Response |
|---|---|
| Topic doesn't exist | "status": "TOPIC_NOT_FOUND" |
| Already subscribed | "status": "ALREADY_EXISTS" |
POST /api/subscribers/bulk
Request
[
{ "email": "nitish.joshi1995@gmail.com", "topicId": 1 },
{ "email": "nitish.rajat1995@gmail.com", "topicId": 1 },
{ "email": "nitish.joshi1995@gmail.com", "topicId": 1 }
]Response
{
"details": [
{
"status": "DUPLICATE_IN_REQUEST",
"topicId": 1,
"email": "nitish.joshi1995@gmail.com"
},
{
"status": "SUBSCRIBED",
"topicId": 1,
"email": "nitish.rajat1995@gmail.com"
},
{
"status": "SUBSCRIBED",
"topicId": 1,
"email": "nitish.joshi1995@gmail.com"
}
],
"status": "SUCCESS",
"message": "All subscribers saved successfully.",
"subscribedCount": 2
}POST /api/content
Request
{
"text": "Breaking News: Market Update",
"topicId": 1,
"scheduledTime": "2025-11-12T14:15:00"
}Response
{
"text": "Breaking News: Market Update",
"topicId": 1,
"status": "PENDING"
}POST /api/content/bulk
Request
[
{
"text": "This is from Nitish",
"scheduledTime": "2025-11-12T15:12:00",
"topicId": 1
},
{
"text": "This is from Nitish Joshi",
"scheduledTime": "2025-11-12T15:16:00",
"topicId": 1
}
]Response
[
{
"text": "This is from Nitish",
"topicId": 1,
"status": "SAVED"
},
{
"text": "This is from Nitish Joshi",
"topicId": 1,
"status": "SAVED"
}
]Request
GET /api/status
Response
[
{
"id": 1,
"status": "PENDING",
"text": "This is from Nitish",
"scheduledTime": "2025-11-12T15:12:00",
"sent": false,
"topic": {
"id": 1,
"name": "Tech News"
}
},
{
"id": 2,
"status": "PENDING",
"text": "This is from Nitish Joshi",
"scheduledTime": "2025-11-12T15:16:00",
"sent": false,
"topic": {
"id": 1,
"name": "Tech News"
}
}
]Request
GET /api/status/{id}
Response
{
"id": 2,
"status": "PENDING",
"text": "This is from Nitish Joshi",
"scheduledTime": "2025-11-12T15:16:00",
"sent": false,
"topic": {
"id": 1,
"name": "Tech News"
}
}Request
GET /api/status/pending
Response
[
{
"id": 2,
"status": "PENDING",
"text": "This is from Nitish Joshi",
"scheduledTime": "2025-11-12T15:16:00",
"sent": false,
"topic": {
"id": 1,
"name": "Tech News"
}
}
]Request
GET /api/status/sent
Response
[
{
"id": 1,
"status": "SENT",
"text": "This is from Nitish",
"scheduledTime": "2025-11-12T15:12:00",
"sent": true,
"topic": {
"id": 1,
"name": "Tech News"
}
},
{
"id": 2,
"status": "SENT",
"text": "This is from Nitish Joshi",
"scheduledTime": "2025-11-12T15:16:00",
"sent": true,
"topic": {
"id": 1,
"name": "Tech News"
}
}
]| Service | Description |
|---|---|
| SubscriberService | Manages subscriber registration and deduplication |
| ContentService | Handles content persistence and scheduling |
| ContentDispatcher | Periodically polls for pending content and dispatches |
| EmailDispatchService | Sends content to subscribers (mock / SMTP / Kafka) |
| DispatchStrategyFactory | Chooses between Kafka / Email strategy |
| TopicRepository, SubscriberRepository, ContentRepository | JPA repositories |
- Stateless beans: All Spring services are stateless and thread-safe.
- Database-level locking: Guarantees no duplicate dispatch.
- ExecutorService / @Async: Enables concurrent email dispatch per topic.
- Safe in multiple EC2 instances: Database enforces unique (email, topicId) keys.
- Transactional updates: Guarantee consistency between send and update.
# Mail configuration
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=nitish.joshi1995@gmail.com
spring.mail.password=**** **** **** ****
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
newsletter.email.from=nitish.joshi1995@gmail.com
# H2 DB (auto creates tables)
spring.datasource.url=jdbc:h2:mem:newsletterdb
spring.datasource.driverClassName=org.h2.Driver
spring.jpa.hibernate.ddl-auto=create
# Dispatcher strategy
newsletter.dispatch.strategy=scheduler # or kafka
# Scheduling strategy
newsletter.scheduler.strategy=fixedRate
# Default email sender type
newsletter.email.sender=gmail- Access the API
- Get EC2 instance public hostname
- Base URL:
http://<EC2_HOST>:8080 - Example:
http://ec2-54-123-45-67.compute-1.amazonaws.com:8080/api/subscribers - Test endpoints using Postman or cURL
- Upon hitting the /content endpoint with a scheduled time, the system should dispatch emails to Gmail (default email provider) at the specified schedule
- Content Creation: Content is created via API with scheduled time
- Scheduler Activation:
@Scheduledmethod runs periodically (every 30s or via cron) - Pending Content Retrieval: Queries database for
PENDINGstatus content - Subscriber Lookup: Fetches all subscribers for the content's topic
- Parallel Dispatch: Uses thread pool to send emails concurrently
- Kafka Event: Publishes dispatch events to
newsletter-eventstopic - Status Update: Updates content status to
SENTorFAILED - Error Handling: Failed dispatches are logged and marked as
FAILED
Author: Nitish Joshi
Version: 1.0.0
Last Updated: November 12, 2025
License: Apache License