Send the newsletter in batches the mail provider accepts
Java · Spring Boot · beginner · modification
Adds the send button's backend for the admin console. `SubscriberRepository` gains `findConfirmedEmails()`, a JPQL projection of confirmed subscribers' emails ordered by id. `NewsletterSendService.send` loads the issue (404 if missing), loads the confirmed emails, cuts them into `subList` batches of `MailProviderClient.MAX_RECIPIENTS` and hands each batch to the provider under the campaign key `newsletter-<issueId>`, then returns a `SendSummary`. `NewsletterAdminController` exposes it as `POST /api/admin/newsletters/{issueId}/send`.
Newsletter backend on Spring Boot 3.3 (Java 21), Spring Data JPA (Hibernate 6) on PostgreSQL, Spring Security. The existing `SecurityConfig` (unchanged) restricts `/api/admin/**` to the `ADMIN` role, and the admin console sends the CSRF token on every POST. Existing and unchanged: the `Subscriber` entity (`id`, `email`, `status`; `email` is non-null and unique), the `SubscriberStatus` enum (`PENDING`, `CONFIRMED`, `UNSUBSCRIBED`), the `NewsletterIssue` entity (`id`, non-null `subject` and `html`) with its `NewsletterIssueRepository`, and the `MailProviderClient` interface with `MAX_RECIPIENTS = 500`. The provider deduplicates recipients per campaign key, so sending the same issue again (for example a retry after a failed call, whose exception propagates and fails the request with a 5xx) does not mail anyone twice. There are at most 200,000 subscribers, so holding their emails in one list is fine. The provider has no rate limit at this volume, and a full 200,000-recipient send finishes well inside the admin endpoint's request timeout (moving the send to a background job is a later change).
Requirements
- `POST /api/admin/newsletters/{issueId}/send` sends the newsletter issue to every subscriber whose status is `CONFIRMED` when the send loads its recipient list, exactly once each (a status change after that applies from the next send). `PENDING` and `UNSUBSCRIBED` subscribers get nothing. An unknown `issueId` returns 404.
- The mail provider accepts at most 500 recipients per `sendBatch` call (`MailProviderClient.MAX_RECIPIENTS`) and rejects a larger call, so the confirmed list is sent in consecutive batches of up to 500. Any number of confirmed subscribers, from 0 up to 200,000, must be fully covered: 0 subscribers means no calls, 1,234 means calls of 500, 500 and 234.
- The response is a `SendSummary` (a record nested in the service): `recipients` is the number of confirmed subscribers, `batches` the number of `sendBatch` calls made.
Files touched
- src/main/java/com/acme/news/SubscriberRepository.java
- src/main/java/com/acme/news/NewsletterSendService.java
- src/main/java/com/acme/news/NewsletterAdminController.java
--- src/main/java/com/acme/news/SubscriberRepository.java
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import java.util.List;
import java.util.Optional;
Optional<Subscriber> findByEmail(String email);
+
+ @Query("select s.email from Subscriber s where s.status = com.acme.news.SubscriberStatus.CONFIRMED order by s.id")
+ List<String> findConfirmedEmails();
}
--- src/main/java/com/acme/news/NewsletterSendService.java
+package com.acme.news;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+import org.springframework.web.server.ResponseStatusException;
+import java.util.List;
+
+@Service
+public class NewsletterSendService {
+
+ public record SendSummary(int recipients, int batches) {
+ }
+
+ private static final int BATCH_SIZE = MailProviderClient.MAX_RECIPIENTS;
+