java 145 lines · 4 tabs

Streaming CSV Import in Spring Boot with MultipartFile and JDBC Batch Inserts

Shared by codesnips Aug 2026
4 tabs
@RestController
@RequestMapping("/api/customers")
public class CsvImportController {

    private final CsvImportService importService;

    public CsvImportController(CsvImportService importService) {
        this.importService = importService;
    }

    @PostMapping(value = "/import", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity<ImportResult> importCsv(@RequestParam("file") MultipartFile file) throws IOException {
        if (file.isEmpty()) {
            return ResponseEntity.badRequest()
                    .body(new ImportResult(0, 0, "Uploaded file is empty"));
        }
        if (!"text/csv".equals(file.getContentType())) {
            return ResponseEntity.unprocessableEntity()
                    .body(new ImportResult(0, 0, "Expected a text/csv file"));
        }

        try (InputStream in = file.getInputStream()) {
            ImportResult result = importService.importCustomers(in);
            return ResponseEntity.ok(result);
        }
    }
}
4 files · java Explain with highlit

This snippet shows how a CSV upload is handled end to end in Spring Boot: an HTTP endpoint accepts the file, a service streams it row by row, and rows are flushed to the database in fixed-size batches rather than one INSERT per line. The batching is the whole point — with a large file, a naive per-row insert incurs one network round trip per record, so import time grows linearly with row count. Batching amortizes that cost by sending hundreds of rows per statement.

In CsvImportController, the endpoint is declared consumes = MULTIPART_FORM_DATA_VALUE and binds the file to a MultipartFile parameter named file. It rejects empty uploads early with a 400, then obtains the raw InputStream and hands it to the service. Nothing is buffered fully into memory here — the controller stays thin and delegates parsing and persistence.

CsvImportService opens a BufferedReader over the stream and uses apache-commons-csv (CSVFormat.DEFAULT with headers) to iterate records lazily. Each parsed line becomes a Customer and is appended to a List buffer. When the buffer reaches BATCH_SIZE, flush is invoked and the list is cleared; a final flush after the loop drains the remainder. The whole method is annotated @Transactional, so either the entire file imports or nothing does — a partial import from a malformed row halfway through is avoided by rolling everything back. The ImportResult returns counts of inserted and skipped rows so the caller gets feedback.

The actual database work lives in CustomerBatchRepository, which wraps a JdbcTemplate.batchUpdate call with a BatchPreparedStatementSetter. That interface exposes getBatchSize and setValues(ps, i), letting the JDBC driver group the parameter sets into one round trip. Using plain JdbcTemplate here, rather than JPA, sidesteps the persistence-context bloat and dirty-checking overhead that make ORMs slow for bulk loads.

A few pitfalls are worth noting: rewriteBatchedStatements=true must be set on the JDBC URL for MySQL to truly collapse the inserts, tuning BATCH_SIZE trades memory against round trips, and per-row validation (the isValid check) lets bad lines be skipped and counted instead of aborting the run. This pattern is the standard reach for importing tens of thousands of rows without exhausting heap or connection time.


Related snips

Share this code

Here's the card — post it anywhere.

Streaming CSV Import in Spring Boot with MultipartFile and JDBC Batch Inserts — share card
Link copied