When would you use CompositeItemWriter versus ClassifierCompositeItemWriter, and how do their write semantics differ?
answer
- Composite = broadcast to ALL delegates
- Classifier = route each item to ONE delegate
- Classifier needs step.stream() for stateful delegates
- order matters in composite
- Classifier<T, ItemWriter> chooses per item
basics
~20 sCompositeItemWriter 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 sBoth 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// 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
Can state composite = write to many, classifier = pick one.
Should give concrete use cases (audit + DB vs valid/invalid routing) and correct per-item semantics.
Should call out the ItemStream registration gotcha for classifier delegates and transaction/side-effect nuances.
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