skip to content

How do you configure a FlatFileItemWriter to produce a CSV, and what are the key options (resource, line aggregator, header/footer, append/restart)?

level: middleimportance: should knowfreq 55%

answer

  1. LineAggregator + FieldExtractor => the line
  2. DelimitedLineAggregator vs FormatterLineAggregator
  3. BeanWrapperFieldExtractor names properties
  4. headerCallback / footerCallback
  5. ItemStream => restart via byte-offset truncate

basics

~20 s

FlatFileItemWriter writes items as text lines to a file Resource. You give it a LineAggregator (e.g. DelimitedLineAggregator with a FieldExtractor) to turn each item into a line, and optionally a header/footer callback. It buffers lines and flushes per chunk.

solid answer

~30 s

FlatFileItemWriter<T> writes each item as one line to a WritableResource. The core piece is the LineAggregator: DelimitedLineAggregator (comma/other delimiter) or FormatterLineAggregator (fixed width), each backed by a FieldExtractor (BeanWrapperFieldExtractor names the properties to pull). You can add a headerCallback and footerCallback for column headers or totals, set encoding, lineSeparator, and shouldDeleteIfExists. For restartability it stores the byte offset in the ExecutionContext, so on restart it truncates back to the last committed position; appendAllowed=true instead appends and disables header on non-empty files. It's an ItemStream, so open/update/close manage the file handle and state. The builder is FlatFileItemWriterBuilder.

code

java · 14 lines
java
@Bean
public FlatFileItemWriter<Person> personWriter() {
    return new FlatFileItemWriterBuilder<Person>()
        .name("personItemWriter")            // required for restart state
        .resource(new FileSystemResource("target/out/people.csv"))
        .encoding("UTF-8")
        .delimited()
        .delimiter(";")
        .names("firstName", "lastName", "age") // -> BeanWrapperFieldExtractor
        .headerCallback(writer -> writer.write("firstName;lastName;age"))
        .footerCallback(writer -> writer.write("# end of file"))
        .shouldDeleteIfExists(true)
        .build();
}

go deeper

for a junior

Can name FlatFileItemWriter and that it writes lines to a file.

for a middle

Should configure resource + LineAggregator + FieldExtractor and mention header/footer and delete-if-exists.

for a senior

Should explain ItemStream restart (byte offset truncation), transactional buffering, append vs overwrite semantics.

for a principal

Should weigh transactional vs forceSync durability trade-offs and restart guarantees in ops/idempotency terms.

**What it is.** `FlatFileItemWriter<T>` is the standard writer for delimited (CSV/TSV) or fixed-width text files. It implements `ResourceAwareItemWriterItemStream`, meaning it is both an `ItemWriter` and an `ItemStream` (lifecycle: `open`, `update`, `close`). **Turning items into lines — LineAggregator.** The writer doesn't know your object's format; it delegates to a `LineAggregator<T>`: - `DelimitedLineAggregator` joins fields with a delimiter (default comma). - `FormatterLineAggregator` uses a `String.format` pattern for fixed-width output. Each aggregator needs a `FieldExtractor<T>` that pulls values out of an item. `BeanWrapperFieldExtractor` takes an array of property names and reads them reflectively; you can also supply a lambda `FieldExtractor`. **Builder example.** ```java new FlatFileItemWriterBuilder<Person>() .name("personWriter") .resource(new FileSystemResource("out/people.csv")) .delimited().delimiter(",") .names("firstName", "lastName", "age") // sets up BeanWrapperFieldExtractor .headerCallback(w -> w.write("firstName,lastName,age")) .build(); ``` **Header/Footer.** `headerCallback` (a `FlatFileHeaderCallback`) runs once when the file is opened — typically to write a column header row. `footerCallback` runs on close, useful for a totals/record-count trailer. **Key options.** - `resource` — the target `WritableResource` (`FileSystemResource`). - `encoding` — set it explicitly (default is UTF-8 in current versions). - `lineSeparator` — default is the system line separator. - `shouldDeleteIfExists` — delete an existing file on open (default true); throws if false and file exists. - `appendAllowed` — append to an existing file rather than overwrite; note when true the header is skipped if the file is non-empty, and `shouldDeleteIfExists` is ignored. - `transactional` — whether buffered writes are held until commit (default true), so a rolled-back chunk isn't flushed to disk. - `forceSync` — fsync on flush for durability. **Restartability.** Because it's an `ItemStream`, on each chunk commit `update()` records the current byte position in the `ExecutionContext`. If the job restarts after a failure, `open()` reads that position and **truncates the file back** to the last committed offset, so no duplicate/partial lines. This only works if `saveState` is true (default) and the writer has a unique `name`. With `appendAllowed=true` this restart-truncate behavior changes to pure append. **Transactional buffering.** By default lines for a chunk are buffered and only flushed on transaction commit, so a failing chunk leaves the file clean. Setting `transactional(false)` writes immediately (faster, but partial chunks can leak on failure). **Gotchas.** - Forgetting `.name(...)` breaks restart state (needed as the ExecutionContext key). - Default deletes an existing file — surprising if you expected append. - Header written via `headerCallback` will be re-run only on a fresh open; on restart of an existing file it is not re-written. - Not setting `encoding` can produce locale-dependent output in older versions. **When to use.** Any time the sink is a text file. For DB use `JdbcBatchItemWriter`/`JpaItemWriter`; for multiple sinks compose with `CompositeItemWriter`.

  • How does FlatFileItemWriter avoid duplicate lines after a job restart?
    It's an ItemStream: on each commit it saves the current byte offset into the ExecutionContext via update(). On restart, open() reads that offset and truncates the file back to it, so writing resumes exactly after the last committed line. Requires a unique name and saveState=true.
  • What is the difference between DelimitedLineAggregator and FormatterLineAggregator?
    DelimitedLineAggregator joins extracted fields with a delimiter (CSV-style). FormatterLineAggregator applies a String.format pattern to produce fixed-width/aligned output. Both rely on a FieldExtractor to get the field values from each item.

saying these in an interview costs you the question

  • Thinking it writes objects directly without a LineAggregator/FieldExtractor
  • Not knowing it deletes an existing file by default
  • Assuming header is re-written on restart
  • Believing it flushes every item immediately (it buffers per chunk / per transaction)

context