java
39 lines · 7 steps
Async report generation with Spring @Async
A Spring service renders PDF reports off the request thread and fans out across accounts using CompletableFuture.
Explained by
highlit
1@Service
2public class ReportGenerationService {
3
4 private final ReportRepository reportRepository;
5 private final PdfRenderer pdfRenderer;
6 private final BlobStorageClient blobStorage;
7
8 public ReportGenerationService(ReportRepository reportRepository,
9 PdfRenderer pdfRenderer,
10 BlobStorageClient blobStorage) {
11 this.reportRepository = reportRepository;
12 this.pdfRenderer = pdfRenderer;
13 this.blobStorage = blobStorage;
14 }
15
16 @Async("reportExecutor")
17 public CompletableFuture<ReportResult> generateMonthlyReport(long accountId, YearMonth period) {
18 var transactions = reportRepository.findByAccountAndPeriod(accountId, period);
19
20 var document = pdfRenderer.render(new ReportModel(accountId, period, transactions));
21 var key = "reports/%d/%s.pdf".formatted(accountId, period);
22 var url = blobStorage.upload(key, document, "application/pdf");
23
24 var result = new ReportResult(accountId, period, url, transactions.size());
25 return CompletableFuture.completedFuture(result);
26 }
27
28 @Async("reportExecutor")
29 public CompletableFuture<List<ReportResult>> generateForAccounts(List<Long> accountIds, YearMonth period) {
30 var futures = accountIds.stream()
31 .map(id -> generateMonthlyReport(id, period))
32 .toList();
33
34 return CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new))
35 .thenApply(ignored -> futures.stream()
36 .map(CompletableFuture::join)
37 .toList());
38 }
39}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Returning CompletableFuture from an @Async method lets Spring run the work on a dedicated executor while callers keep a handle to the result.
- 2CompletableFuture.allOf plus join is the standard pattern for waiting on a batch of parallel tasks and collecting their results.
- 3Naming the executor in @Async keeps report work on its own thread pool, isolating it from other async tasks.
Related explainers
javascript
import { unstable_cache, revalidateTag } from 'next/cache' import { db } from '@/lib/db' export const getDashboardStats = unstable_cache(
Caching dashboard stats in Next.js
caching
cache-invalidation
tag-based-revalidation
Intermediate
8 steps
java
@Component @Order(Ordered.HIGHEST_PRECEDENCE) public class TenantResolutionFilter extends OncePerRequestFilter {
How a tenant-resolution filter works in Spring
multi-tenancy
servlet-filter
thread-local
Intermediate
8 steps
java
@Repository public interface SubscriptionRepository extends JpaRepository<Subscription, Long> { @Modifying(clearAutomatically = true, flushAutomatically = true)
Bulk JPQL updates in a Spring Data repository
jpql
bulk-update
modifying-query
Intermediate
5 steps
java
package com.example.uploads.config; import jakarta.servlet.MultipartConfigElement; import org.springframework.boot.web.servlet.MultipartConfigFactory;
Configuring upload size limits in Spring
file-upload
exception-handling
http-errors
Intermediate
6 steps
java
public final class Levenshtein { private Levenshtein() { }
Two-row Levenshtein distance in Java
dynamic-programming
edit-distance
space-optimization
Intermediate
8 steps
go
package editor import ( "context"
How a debouncer coalesces bursts in Go
debounce
concurrency
timers
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/async-report-generation-with-spring-async-explained-java-015c/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.