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
go
package pipeline import ( "context"
A cancelable worker pool in Go
concurrency
channels
worker-pool
Advanced
8 steps
python
from fastapi import Depends, FastAPI, HTTPException from sqlalchemy import create_engine from sqlalchemy.orm import Session, sessionmaker
Wiring SQLAlchemy sessions into FastAPI
dependency-injection
orm
sessions
Intermediate
7 steps
java
package com.example.billing; import java.math.BigDecimal; import java.math.RoundingMode;
Locale-aware currency formatting in Java
internationalization
rounding
immutability
Intermediate
6 steps
java
@Service public class ProductCatalogService { private final ProductRepository productRepository;
How Spring cache annotations keep data fresh
caching
cache-invalidation
dependency-injection
Intermediate
7 steps
java
@RestController @RequestMapping("/api/users") public class UserController {
Bean Validation in a Spring REST controller
validation
rest-api
exception-handling
Intermediate
9 steps
java
@Service public class OrderService { private final OrderRepository orderRepository;
Decoupling side effects with Spring events
event-driven
transactions
decoupling
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.