diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/kafka/KafkaItemReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/kafka/KafkaItemReader.java
new file mode 100644
index 000000000..54ba0e7c0
--- /dev/null
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/kafka/KafkaItemReader.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.batch.item.kafka;
+
+import java.time.Duration;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import org.apache.kafka.clients.consumer.Consumer;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.apache.kafka.common.TopicPartition;
+import org.springframework.batch.item.ExecutionContext;
+import org.springframework.batch.item.ItemReader;
+import org.springframework.batch.item.ItemStreamException;
+import org.springframework.batch.item.kafka.support.AutoCommitOffsetsProvider;
+import org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.kafka.core.ConsumerFactory;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+
+/**
+ *
+ * An {@link ItemReader} implementation for Apache Kafka.
+ *
+ *
+ * @author Mathieu Ouellet
+ * @since 4.2
+ *
+ */
+public class KafkaItemReader extends AbstractItemCountingItemStreamItemReader implements InitializingBean {
+
+ private static final String TOPIC_PARTITION_OFFSET = "topic.partition.offset";
+
+ private static final long DEFAULT_POLL_TIMEOUT = 50L;
+
+ private static final long MIN_ASSIGN_TIMEOUT = 2000L;
+
+ private final Supplier assignTimeoutProvider = () -> Duration
+ .ofMillis(Math.max(this.pollTimeout.toMillis() * 20, MIN_ASSIGN_TIMEOUT));
+
+ private Duration pollTimeout = Duration.ofMillis(DEFAULT_POLL_TIMEOUT);
+
+ private List topicPartitions;
+
+ private List topics;
+
+ private ConsumerFactory consumerFactory;
+
+ private Consumer consumer;
+
+ private OffsetsProvider offsetsProvider;
+
+ private AtomicBoolean assigned = new AtomicBoolean(false);
+
+ private Map offsets;
+
+ private Iterator> records;
+
+ public KafkaItemReader() {
+ super();
+ setName(ClassUtils.getShortName(KafkaItemReader.class));
+ }
+
+ public void setPollTimeout(long pollTimeout) {
+ Assert.isTrue(pollTimeout >= 0, "'pollTimeout' must no be negative.");
+ this.pollTimeout = Duration.ofMillis(pollTimeout);
+ }
+
+ public void setTopicPartitions(List topicPartitions) {
+ this.topicPartitions = topicPartitions;
+ }
+
+ public void setTopics(List topics) {
+ this.topics = topics;
+ }
+
+ public void setConsumerFactory(ConsumerFactory consumerFactory) {
+ this.consumerFactory = consumerFactory;
+ }
+
+ public void setOffsetsProvider(OffsetsProvider offsetsProvider) {
+ this.offsetsProvider = offsetsProvider;
+ }
+
+ @Override
+ public void afterPropertiesSet() throws Exception {
+ Assert.state(topicPartitions != null || topics != null, "Either 'topicPartitions' or 'topics' must be provided.");
+ Assert.state(topicPartitions == null || topics == null, "Both 'topicPartitions' and 'topics' cannot be specified together.");
+ Assert.notNull(consumerFactory, "'consumerFactory' must not be null.");
+ Assert.notNull(offsetsProvider, "'offsetsProvider' must not be null.");
+ if (consumerFactory.isAutoCommit()) {
+ Assert.state(offsetsProvider instanceof AutoCommitOffsetsProvider, "'AutoCommitOffsetsProvider' must be used if 'consumerFactory' is set to auto commit.");
+ }
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void open(ExecutionContext executionContext) throws ItemStreamException {
+ super.open(executionContext);
+ try {
+ if (isSaveState() && executionContext.containsKey(TOPIC_PARTITION_OFFSET)) {
+ offsets = (Map) executionContext.get(TOPIC_PARTITION_OFFSET);
+ }
+ else {
+ offsets = offsetsProvider.get(topicPartitions);
+ }
+
+ if (offsets != null && !offsets.isEmpty()) {
+ offsets.forEach(consumer::seek);
+ }
+ }
+ catch (Exception e) {
+ throw new ItemStreamException("Failed to initialize the reader", e);
+ }
+ }
+
+ @Override
+ protected void doOpen() throws Exception {
+ consumer = consumerFactory.createConsumer();
+ if (topics != null) {
+ topicPartitions = topics.stream()
+ .flatMap(topic -> consumer.partitionsFor(topic).stream())
+ .map(partitionInfo -> new TopicPartition(partitionInfo.topic(), partitionInfo.partition()))
+ .collect(Collectors.toList());
+ }
+ consumer.assign(topicPartitions);
+ offsetsProvider.setConsumer(consumer);
+ }
+
+ @Override
+ protected void jumpToItem(int itemIndex) throws Exception {
+ }
+
+ @Override
+ protected V doRead() throws Exception {
+ if (records == null || !records.hasNext()) {
+ records = doPoll();
+ }
+ if (records.hasNext()) {
+ ConsumerRecord record = records.next();
+ offsets.put(new TopicPartition(record.topic(), record.partition()), record.offset());
+ return record.value();
+ }
+ return null;
+ }
+
+ protected Iterator> doPoll() {
+ return consumer.poll(assigned.getAndSet(true) ? pollTimeout : assignTimeoutProvider.get()).iterator();
+ }
+
+ @Override
+ public void update(ExecutionContext executionContext) throws ItemStreamException {
+ super.update(executionContext);
+ if (isSaveState()) {
+ Assert.notNull(executionContext, "ExecutionContext must not be null");
+ executionContext.put(TOPIC_PARTITION_OFFSET, offsets);
+ }
+ }
+
+ @Override
+ protected void doClose() throws Exception {
+ records = null;
+ offsets = null;
+ assigned.set(false);
+ if (consumer != null) {
+ consumer.close();
+ }
+ }
+}
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/kafka/OffsetsProvider.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/kafka/OffsetsProvider.java
new file mode 100644
index 000000000..7c898624b
--- /dev/null
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/kafka/OffsetsProvider.java
@@ -0,0 +1,37 @@
+package org.springframework.batch.item.kafka;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.kafka.clients.consumer.Consumer;
+import org.apache.kafka.common.TopicPartition;
+
+/**
+ * A convenient strategy for providing initial externally stored offsets for the {@link KafkaItemReader} to
+ * seeks to.
+ *
+ * @author Mathieu Ouellet
+ * @since 4.2
+ */
+public interface OffsetsProvider {
+
+ /**
+ *
+ * Map of assigned topic-partitions and the offset, or read position, at which the {@link KafkaItemReader} should
+ * start.
+ *
+ *
+ * @param topicPartitions list of assigned topic-partitions to get offset for
+ * @return map of offset by topic-partition
+ */
+ Map get(List topicPartitions);
+
+ /**
+ *
+ * Inject a {@link Consumer} that can be used to fetch internally stored offsets and/or topic-partition assignment.
+ *
+ *
+ * @param consumer the {@link Consumer} to set
+ */
+ void setConsumer(Consumer, ?> consumer);
+}
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/kafka/builder/KafkaItemReaderBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/kafka/builder/KafkaItemReaderBuilder.java
new file mode 100644
index 000000000..f2fbe4f5b
--- /dev/null
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/kafka/builder/KafkaItemReaderBuilder.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.batch.item.kafka.builder;
+
+import java.util.List;
+
+import org.apache.kafka.clients.consumer.Consumer;
+import org.apache.kafka.common.TopicPartition;
+import org.springframework.batch.item.kafka.KafkaItemReader;
+import org.springframework.batch.item.kafka.OffsetsProvider;
+import org.springframework.batch.item.kafka.support.AutoCommitOffsetsProvider;
+import org.springframework.kafka.core.ConsumerFactory;
+import org.springframework.util.Assert;
+
+/**
+ * A builder implementation for the {@link KafkaItemReader}
+ *
+ * @author Mathieu Ouellet
+ * @since 4.2
+ * @see KafkaItemReader
+ */
+public class KafkaItemReaderBuilder {
+
+ private ConsumerFactory consumerFactory;
+
+ private List topicPartitions;
+
+ private List topics;
+
+ private OffsetsProvider offsetsProvider = new AutoCommitOffsetsProvider();
+
+ private long pollTimeout = 50L;
+
+ private boolean saveState = true;
+
+ private String name;
+
+ private int maxItemCount = Integer.MAX_VALUE;
+
+ /**
+ * Configure if the state of the {@link org.springframework.batch.item.ItemStreamSupport} should be persisted within
+ * the {@link org.springframework.batch.item.ExecutionContext} for restart purposes.
+ *
+ * @param saveState defaults to true
+ * @return The current instance of the builder.
+ */
+ public KafkaItemReaderBuilder saveState(boolean saveState) {
+ this.saveState = saveState;
+ return this;
+ }
+
+ /**
+ * The name used to calculate the key within the {@link org.springframework.batch.item.ExecutionContext}. Required
+ * if {@link #saveState(boolean)} is set to true.
+ *
+ * @param name name of the reader instance
+ * @return The current instance of the builder.
+ * @see org.springframework.batch.item.ItemStreamSupport#setName(String)
+ */
+ public KafkaItemReaderBuilder name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ /**
+ * Configure the max number of items to be read.
+ *
+ * @param maxItemCount the max items to be read
+ * @return The current instance of the builder.
+ * @see org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader#setMaxItemCount(int)
+ */
+ public KafkaItemReaderBuilder maxItemCount(int maxItemCount) {
+ this.maxItemCount = maxItemCount;
+ return this;
+ }
+
+ /**
+ * The {@link ConsumerFactory} implementation to produce a new {@link Consumer} instance for the reader.
+ *
+ * @param consumerFactory
+ * @return The current instance of the builder.
+ * @see KafkaItemReader#setConsumerFactory(ConsumerFactory)
+ */
+ public KafkaItemReaderBuilder consumerFactory(ConsumerFactory consumerFactory) {
+ this.consumerFactory = consumerFactory;
+ return this;
+ }
+
+ /**
+ * A list of {@link TopicPartition}s to manually assign the consumer.
+ *
+ * @param topicPartitions list of partitions to assign the consumer
+ * @return The current instance of the builder.
+ * @see KafkaItemReader#setTopicPartitions(List)
+ */
+ public KafkaItemReaderBuilder topicPartitions(List topicPartitions) {
+ this.topicPartitions = topicPartitions;
+ return this;
+ }
+
+ /**
+ * A list of topics to manually assign the consumer.
+ *
+ * @param topics list of topics to assign the consumer
+ * @return The current instance of the builder.
+ * @see KafkaItemReader#setTopics(List)
+ */
+ public KafkaItemReaderBuilder topics(List topics) {
+ this.topics = topics;
+ return this;
+ }
+
+ /**
+ * The {@link OffsetsProvider} implementation to provide initial offsets.
+ *
+ * @param offsetsProvider
+ * @return The current instance of the builder.
+ * @see KafkaItemReader#setOffsetsProvider(OffsetsProvider)
+ */
+ public KafkaItemReaderBuilder offsetsProvider(OffsetsProvider offsetsProvider) {
+ this.offsetsProvider = offsetsProvider;
+ return this;
+ }
+
+ /**
+ * Set the pollTimeout for the poll() operations.
+ *
+ * @param pollTimeout default to 50ms
+ * @return The current instance of the builder.
+ * @see KafkaItemReader#setPollTimeout(long)
+ */
+ public KafkaItemReaderBuilder pollTimeout(long pollTimeout) {
+ this.pollTimeout = pollTimeout;
+ return this;
+ }
+
+ public KafkaItemReader build() {
+ if (this.saveState) {
+ Assert.hasText(this.name, "A name is required when saveState is set to true");
+ }
+ Assert.state(this.topicPartitions != null || this.topics != null, "Either 'topicPartitions' or 'topics' must be provided.");
+ Assert.state(this.topicPartitions == null || this.topics == null, "Both 'topicPartitions' and 'topics' cannot be specified together.");
+ Assert.isTrue(this.pollTimeout >= 0, "pollTimeout must not be negative.");
+ Assert.notNull(this.consumerFactory, "'consumerFactory' must not be null.");
+ Assert.notNull(this.offsetsProvider, "'offsetsProvider' must not be null.");
+ if (this.consumerFactory.isAutoCommit()) {
+ Assert.state(this.offsetsProvider instanceof AutoCommitOffsetsProvider, "'AutoCommitOffsetsProvider' must be used if 'consumerFactory' is set to auto commit.");
+ }
+
+ KafkaItemReader reader = new KafkaItemReader<>();
+ reader.setConsumerFactory(this.consumerFactory);
+ reader.setTopicPartitions(this.topicPartitions);
+ reader.setTopics(this.topics);
+ reader.setOffsetsProvider(this.offsetsProvider);
+ reader.setPollTimeout(this.pollTimeout);
+ reader.setSaveState(this.saveState);
+ reader.setName(this.name);
+ reader.setMaxItemCount(this.maxItemCount);
+ return reader;
+ }
+}
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/kafka/support/AutoCommitOffsetsProvider.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/kafka/support/AutoCommitOffsetsProvider.java
new file mode 100644
index 000000000..630d2858e
--- /dev/null
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/kafka/support/AutoCommitOffsetsProvider.java
@@ -0,0 +1,32 @@
+package org.springframework.batch.item.kafka.support;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.kafka.clients.consumer.Consumer;
+import org.apache.kafka.common.TopicPartition;
+import org.springframework.batch.item.kafka.OffsetsProvider;
+
+/**
+ * Noop implementation of {@link OffsetsProvider} to use with automatic offset committing. This is the only
+ * implementation allowed when the KafkaConsumer has 'enable.auto.commit' set to true. See 'auto.offset.reset' config
+ * for initial offset strategy options.
+ *
+ * @author Mathieu Ouellet
+ * @since 4.2
+ */
+public class AutoCommitOffsetsProvider implements OffsetsProvider {
+
+ /**
+ * @return null, preventing a call to
+ * {@link org.apache.kafka.clients.consumer.Consumer#seek(org.apache.kafka.common.TopicPartition, long)}
+ */
+ @Override
+ public Map get(List topicPartitions) {
+ return null;
+ }
+
+ @Override
+ public void setConsumer(Consumer, ?> consumer) {
+ }
+}
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/kafka/support/BeginningOffsetsProvider.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/kafka/support/BeginningOffsetsProvider.java
new file mode 100644
index 000000000..2a518fb16
--- /dev/null
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/kafka/support/BeginningOffsetsProvider.java
@@ -0,0 +1,34 @@
+package org.springframework.batch.item.kafka.support;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.kafka.clients.consumer.Consumer;
+import org.apache.kafka.common.TopicPartition;
+import org.springframework.batch.item.kafka.OffsetsProvider;
+
+/**
+ *
+ * Implementation of {@link OffsetsProvider} that returns the earliest offsets for the given topic-partitions.
+ * Equivalent of using 'auto.offset.reset' set to 'earliest' with 'enable.auto.commit' set to false.
+ *