skip to content

When would you use CompositeItemWriter versus ClassifierCompositeItemWriter, and how do their write semantics differ?

level: seniorimportance: should knowfreq 40%

answer

  1. Composite = broadcast to ALL delegates
  2. Classifier = route each item to ONE delegate
  3. Classifier needs step.stream() for stateful delegates
  4. order matters in composite
  5. Classifier<T, ItemWriter> chooses per item

basics

~20 s

CompositeItemWriter sends every item to all its delegate writers in order (fan-out — write to DB and a file). ClassifierCompositeItemWriter uses a Classifier to route each item to one writer based on the item, so different items go to different destinations.

solid answer

~40 s

Both are ItemWriters that wrap other writers. CompositeItemWriter holds an ordered list of delegates and, for each chunk, passes the whole chunk to every delegate in sequence — so each item is written to all destinations (e.g. persist to a table AND append to an audit file). ClassifierCompositeItemWriter instead holds a Classifier<T, ItemWriter<? super T>>: it partitions the incoming chunk by classifier result and forwards each sub-group to its matching delegate, so each item goes to exactly one destination (route valid vs invalid records, or shard by type/region). Composite = fan-out/broadcast; Classifier = routing/partitioning. Stateful delegates (FlatFileItemWriter) must be registered as ItemStreams on the step via stream(); CompositeItemWriter propagates the stream lifecycle to delegates, but ClassifierCompositeItemWriter does not, so you must register those delegates yourself.

code

java · 21 lines
java
// Fan-out: every item to DB and to a file
@Bean
CompositeItemWriter<Trade> composite(JdbcBatchItemWriter<Trade> db,
                                     FlatFileItemWriter<Trade> file) {
    return new CompositeItemWriterBuilder<Trade>()
        .delegates(db, file)   // each item written to BOTH
        .build();
}

// Routing: valid vs invalid go to different writers
@Bean
ClassifierCompositeItemWriter<Trade> router(ItemWriter<Trade> valid,
                                            ItemWriter<Trade> rejected) {
    ClassifierCompositeItemWriter<Trade> w = new ClassifierCompositeItemWriter<>();
    w.setClassifier((Classifier<Trade, ItemWriter<? super Trade>>)
        t -> t.getPrice() >= 0 ? valid : rejected);
    return w;
}

// Register stateful delegates so their streams open/close:
// stepBuilder.stream(file).stream(valid).stream(rejected)

go deeper

for a junior

Can state composite = write to many, classifier = pick one.

for a middle

Should give concrete use cases (audit + DB vs valid/invalid routing) and correct per-item semantics.

for a senior

Should call out the ItemStream registration gotcha for classifier delegates and transaction/side-effect nuances.

for a principal

Should design nested composites, reason about idempotency across mixed transactional/non-transactional sinks and failure ordering.

**CompositeItemWriter<T> — fan-out.** - Configured with a list of delegate `ItemWriter`s. - On `write(chunk)`, it iterates its delegates **in order** and calls `delegate.write(chunk)` on each with the *same* chunk. Every item is therefore written to *every* delegate. - Use when one item must land in multiple sinks: e.g. insert into a DB table and also write a CSV audit trail, or write to two tables. - Ordering matters if delegates have dependencies; failure in a later delegate rolls back the chunk transaction (assuming shared transactional resources), but a non-transactional file delegate may already have side effects. **ClassifierCompositeItemWriter<T> — routing.** - Configured with a `Classifier<C, ItemWriter<? super C>>` (often a `BackToBackPatternClassifier` or a custom lambda) that, given an item, returns the delegate writer that should handle it. - On `write(chunk)`, it groups the chunk's items by their classified writer and calls each selected writer once with only its subset. Each item goes to exactly **one** delegate. - Use when different items need different destinations: valid vs rejected records to different files/tables, sharding by customer type/region, or writing different entity subtypes to different tables. **Key semantic difference.** - Composite: item -> ALL delegates (broadcast). - Classifier: item -> ONE delegate chosen per item (partition/route). **ItemStream lifecycle gotcha.** Delegates like `FlatFileItemWriter` are `ItemStream`s needing `open`/`update`/`close` for file handles and restart state. `CompositeItemWriter` **does** implement `ItemStream` and delegates lifecycle to any delegates that are streams. `ClassifierCompositeItemWriter` does **not** manage its delegates' streams (the classifier can produce writers it never sees at open time), so you must register each stateful delegate with the step via `.stream(delegateWriter)` in the step builder, otherwise the file is never opened/closed and you get errors or lost restart state. **Transactions.** All delegates run inside the single chunk transaction. Non-transactional sinks (files with `transactional(false)`, external calls) can leave partial side effects if a later delegate fails; keep this in mind for idempotency. **Builders.** `CompositeItemWriterBuilder<T>().delegates(w1, w2).build()`; the classifier variant is usually wired directly with `setClassifier(...)`. **When to use which.** - Need the same record in several places -> CompositeItemWriter. - Need to send different records to different places -> ClassifierCompositeItemWriter. - Need both -> a classifier whose delegates are themselves composites.

  • Your ClassifierCompositeItemWriter routes to two FlatFileItemWriters but you get a 'writer must be open' error. Why?
    The classifier writer doesn't propagate ItemStream lifecycle to delegates it selects at runtime. You must register each FlatFileItemWriter with the step via .stream(writerA).stream(writerB) so open/update/close (and restart state) are managed.
  • Can you combine both patterns?
    Yes. Use a ClassifierCompositeItemWriter whose delegates are themselves CompositeItemWriters, so routed items are then broadcast to multiple sinks — e.g. route by region, and within each region write to both a table and a file.

saying these in an interview costs you the question

  • Saying CompositeItemWriter routes items to one writer (that's the classifier)
  • Saying ClassifierCompositeItemWriter broadcasts to all (that's composite)
  • Forgetting classifier delegates need step.stream() registration
  • Assuming file side effects roll back like DB writes when transactional=false

context