Apache POI is the standard Java library for reading and writing Excel files. Converting CSV to XLSX means parsing the CSV yourself (Apache Commons CSV is handy here) and writing rows into an XSSFWorkbook. For files over 100,000 rows, switch to SXSSFWorkbook — it keeps only a sliding window of rows in memory instead of building the entire workbook in RAM. For minimal-dependency environments, the ChangeThisFile API handles the full conversion from a HttpClient POST.

Method 1: Apache POI (XSSFWorkbook)

Apache POI writes XLSX natively. Apache Commons CSV handles the parsing (quoting, escaping, multi-line fields).

<dependency>
  <groupId>org.apache.poi</groupId>
  <artifactId>poi-ooxml</artifactId>
  <version>5.3.0</version>
</dependency>
<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-csv</artifactId>
  <version>1.11.0</version>
</dependency>
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVRecord;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.*;
import java.nio.file.Path;
import java.util.List;

public class CsvToXlsx {

    public static void convert(Path csvPath, Path xlsxPath) throws IOException {
        try (Reader reader = new FileReader(csvPath.toFile());
             Workbook wb = new XSSFWorkbook()) {

            Sheet sheet = wb.createSheet("Data");
            Iterable<CSVRecord> records = CSVFormat.DEFAULT
                .withFirstRecordAsHeader()
                .withTrim()
                .parse(reader);

            // Style the header row
            CellStyle headerStyle = wb.createCellStyle();
            Font font = wb.createFont();
            font.setBold(true);
            headerStyle.setFont(font);
            headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
            headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);

            boolean headerWritten = false;
            int rowIdx = 0;

            for (CSVRecord record : records) {
                if (!headerWritten) {
                    // Write header row
                    Row headerRow = sheet.createRow(rowIdx++);
                    List<String> headers = record.getParser().getHeaderNames();
                    for (int col = 0; col < headers.size(); col++) {
                        Cell cell = headerRow.createCell(col);
                        cell.setCellValue(headers.get(col));
                        cell.setCellStyle(headerStyle);
                    }
                    headerWritten = true;
                }

                Row row = sheet.createRow(rowIdx++);
                for (int col = 0; col < record.size(); col++) {
                    String val = record.get(col);
                    Cell cell = row.createCell(col);
                    // Try numeric first, fall back to string
                    try {
                        cell.setCellValue(Double.parseDouble(val));
                    } catch (NumberFormatException e) {
                        cell.setCellValue(val);
                    }
                }
            }

            // Auto-size columns (only practical for <50 columns)
            if (sheet.getRow(0) != null) {
                for (int col = 0; col < sheet.getRow(0).getLastCellNum(); col++) {
                    sheet.autoSizeColumn(col);
                }
            }

            try (FileOutputStream out = new FileOutputStream(xlsxPath.toFile())) {
                wb.write(out);
            }
        }
    }

    public static void main(String[] args) throws IOException {
        convert(Path.of("data.csv"), Path.of("data.xlsx"));
        System.out.println("Converted to data.xlsx");
    }
}

autoSizeColumn() measures rendered column widths — it's slow on large sheets. Skip it or limit it to the first 20 columns for performance.

Large files: SXSSFWorkbook (streaming, low memory)

For CSVs with 100k+ rows, XSSFWorkbook builds the entire workbook in heap — you'll hit OOM on large files. SXSSFWorkbook keeps only a bounded window of rows in memory and flushes to disk as rows are added.

import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVRecord;

import java.io.*;
import java.nio.file.Path;

public class LargeCsvToXlsx {

    public static void convert(Path csvPath, Path xlsxPath) throws IOException {
        // Row window of 1000 — rows beyond this are flushed to temp disk
        try (Reader reader = new FileReader(csvPath.toFile());
             SXSSFWorkbook wb = new SXSSFWorkbook(1000)) {

            var sheet = wb.createSheet("Data");
            var records = CSVFormat.DEFAULT.withFirstRecordAsHeader().parse(reader);
            int rowIdx = 0;

            for (CSVRecord record : records) {
                var row = sheet.createRow(rowIdx++);
                for (int col = 0; col < record.size(); col++) {
                    row.createCell(col).setCellValue(record.get(col));
                }
            }

            try (FileOutputStream out = new FileOutputStream(xlsxPath.toFile())) {
                wb.write(out);
            }
            wb.dispose(); // Clean up temp files
        }
    }
}

Note: SXSSFWorkbook does not support autoSizeColumn() — column widths must be set manually or skipped.

Method 2: ChangeThisFile API (Java 11 HttpClient, no SDK)

Post the CSV as multipart to the API. Source is auto-detected from the file content. Free tier covers 1,000 conversions/month.

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class CsvToXlsxApi {

    private static final String API_KEY = "ctf_sk_your_key_here";
    private static final String API_URL = "https://changethisfile.com/v1/convert";
    private static final HttpClient HTTP = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(10))
        .build();

    public static byte[] convert(Path csvPath) throws IOException, InterruptedException {
        String boundary = "----CTFBoundary" + UUID.randomUUID().toString().replace("-", "");
        byte[] fileBytes = Files.readAllBytes(csvPath);

        List<byte[]> parts = new ArrayList<>();
        parts.add(("--" + boundary + "\r\n" +
            "Content-Disposition: form-data; name=\"target\"\r\n\r\nxlsx\r\n").getBytes(StandardCharsets.UTF_8));
        parts.add(("--" + boundary + "\r\n" +
            "Content-Disposition: form-data; name=\"file\"; filename=\"" + csvPath.getFileName() + "\"\r\n" +
            "Content-Type: text/csv\r\n\r\n").getBytes(StandardCharsets.UTF_8));
        parts.add(fileBytes);
        parts.add(("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));

        int totalLen = parts.stream().mapToInt(b -> b.length).sum();
        byte[] body = new byte[totalLen];
        int offset = 0;
        for (byte[] part : parts) {
            System.arraycopy(part, 0, body, offset, part.length);
            offset += part.length;
        }

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(API_URL))
            .header("Authorization", "Bearer " + API_KEY)
            .header("Content-Type", "multipart/form-data; boundary=" + boundary)
            .timeout(Duration.ofSeconds(60))
            .POST(HttpRequest.BodyPublishers.ofByteArray(body))
            .build();

        HttpResponse<byte[]> response = HTTP.send(request,
            HttpResponse.BodyHandlers.ofByteArray());

        if (response.statusCode() != 200) {
            throw new IOException("API error " + response.statusCode() +
                ": " + new String(response.body()));
        }
        return response.body();
    }

    public static void main(String[] args) throws Exception {
        byte[] xlsx = convert(Path.of("data.csv"));
        Files.write(Path.of("data.xlsx"), xlsx);
        System.out.println("Saved data.xlsx (" + xlsx.length + " bytes)");
    }
}

When to use each

ApproachBest forTradeoff
Apache POI (XSSFWorkbook)Full control: styles, formulas, merged cells, column widthsLoads full workbook in heap — OOM above ~200k rows
Apache POI (SXSSFWorkbook)Large CSVs (100k+ rows), streaming outputNo autoSizeColumn; random access to rows not supported
ChangeThisFile APIZero deps, serverless, quick integrationNetwork latency; 25MB CSV limit on free tier

Production tips

  • Use SXSSFWorkbook for anything over 50k rows. XSSFWorkbook allocates all cells in heap — a 200k-row CSV can consume 500MB+. SXSSFWorkbook streams to disk transparently.
  • Detect numeric columns upfront. Scan the first 100 rows to determine if a column is numeric, date, or text. Setting the correct cell type prevents Excel from showing "123" as text.
  • Wrap row-writing in a CompletableFuture pipeline for batch jobs. Parse CSVs in parallel, write XLSX files sequentially to avoid concurrent disk I/O contention.
  • Remember to call wb.dispose() on SXSSFWorkbook. This cleans up the temporary disk files the streaming workbook creates. Not calling it leaks temp files.
  • Reuse HttpClient for API calls. Create one static instance — it maintains connection pooling across requests to the same host.

Apache POI is the right library for CSV-to-XLSX in Java — it gives you full control over styles, formulas, and column widths. For large files, switch to SXSSFWorkbook to avoid OOM. When your environment can't carry the POI dependency (Lambda, shared runtimes), the ChangeThisFile API handles it with Java 11's built-in HttpClient. Free tier covers 1,000 conversions/month.