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
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
python
from fastapi import FastAPI, WebSocket, WebSocketDisconnect app = FastAPI()
Building a WebSocket chat with FastAPI
websockets
broadcast
connection-management
Intermediate
9 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
java
@Component @Converter public class EncryptedStringConverter implements AttributeConverter<String, String> {
Transparent column encryption in Spring & JPA
encryption
aes-gcm
jpa-converter
Advanced
10 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
rust
use axum::{ extract::{Path, State}, response::sse::{Event, KeepAlive, Sse}, };
Streaming import progress with SSE in Axum
server-sent-events
streams
watch-channel
Advanced
7 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.