diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cql/core/support/CachedPreparedStatementCreator.java b/spring-data-cassandra/src/main/java/org/springframework/data/cql/core/support/CachedPreparedStatementCreator.java
new file mode 100644
index 000000000..1fd15d091
--- /dev/null
+++ b/spring-data-cassandra/src/main/java/org/springframework/data/cql/core/support/CachedPreparedStatementCreator.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2017 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.data.cql.core.support;
+
+import org.springframework.data.cql.core.PreparedStatementCreator;
+import org.springframework.data.cql.core.QueryOptions;
+import org.springframework.data.cql.core.QueryOptionsUtil;
+import org.springframework.util.Assert;
+
+import com.datastax.driver.core.PreparedStatement;
+import com.datastax.driver.core.RegularStatement;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.core.SimpleStatement;
+import com.datastax.driver.core.exceptions.DriverException;
+
+/**
+ * {@link PreparedStatementCreator} implementation using caching of prepared statements.
+ *
+ * Regular CQL statements are prepared on first use and executed as prepared statements. Prepared statements are cached
+ * by Cassandra itself (invalidation/eviction possible), in the driver to be able to re-prepare a statement and in this
+ * {@link CachedPreparedStatementCreator} using {@link PreparedStatementCache}.
+ *
+ * @author Mark Paluch
+ * @since 2.0
+ * @see PreparedStatementCache
+ */
+public class CachedPreparedStatementCreator implements PreparedStatementCreator {
+
+ private final PreparedStatementCache cache;
+
+ private final RegularStatement statement;
+
+ /**
+ * Create a new {@link CachedPreparedStatementCreator}.
+ *
+ * @param cache must not be {@literal null}.
+ * @param statement may be {@literal null} of {@code cql} is provided.
+ */
+ protected CachedPreparedStatementCreator(PreparedStatementCache cache, RegularStatement statement) {
+
+ Assert.notNull(cache, "Cache must not be null");
+
+ this.cache = cache;
+ this.statement = statement;
+ }
+
+ /**
+ * Create a new {@link CachedPreparedStatementCreator} given {@link PreparedStatementCache} and
+ * {@link RegularStatement} to prepare. Subsequent calls require the a {@link RegularStatement} object with the same
+ * CQL test for a cache hit. Otherwise, the statement is likely to be re-prepared.
+ *
+ * @param cache must not be {@literal null}.
+ * @param cql must not be {@literal null} or empty.
+ * @return the {@link CachedPreparedStatementCreator} for {@link RegularStatement}.
+ */
+ public static CachedPreparedStatementCreator of(PreparedStatementCache cache, RegularStatement statement) {
+
+ Assert.notNull(cache, "Cache must not be null");
+ Assert.notNull(statement, "Statement must not be null");
+
+ return new CachedPreparedStatementCreator(cache, statement);
+ }
+
+ /**
+ * Create a new {@link CachedPreparedStatementCreator} given {@link PreparedStatementCache} and {@code cql} to
+ * prepare. Subsequent calls require the a CQL statement that {@link String#equals(Object) are equal} to the
+ * previously used CQL string for a cache hit. Otherwise, the statement is likely to be re-prepared.
+ *
+ * @param cache must not be {@literal null}.
+ * @param cql must not be {@literal null} or empty.
+ * @return the {@link CachedPreparedStatementCreator} for {@code cql}.
+ */
+ public static CachedPreparedStatementCreator of(PreparedStatementCache cache, String cql) {
+
+ Assert.notNull(cache, "Cache must not be null");
+ Assert.hasText(cql, "CQL statement must not be null");
+
+ return new CachedPreparedStatementCreator(cache, new SimpleStatement(cql));
+ }
+
+ /**
+ * Create a new {@link CachedPreparedStatementCreator} given {@link PreparedStatementCache} and {@code cql} to
+ * prepare. This method applies {@link QueryOptions} to the {@link com.datastax.driver.core.Statement} before
+ * preparing it. Subsequent calls require the a CQL statement that {@link String#equals(Object) are equal} to the
+ * previously used CQL string for a cache hit. Otherwise, the statement is likely to be re-prepared.
+ *
+ * @param cache must not be {@literal null}.
+ * @param cql must not be {@literal null} or empty.
+ * @param queryOptions must not be {@literal null}.
+ * @return the {@link CachedPreparedStatementCreator} for {@code cql}.
+ */
+ public static CachedPreparedStatementCreator of(PreparedStatementCache cache, String cql, QueryOptions queryOptions) {
+
+ Assert.notNull(cache, "Cache must not be null");
+ Assert.hasText(cql, "CQL statement must not be null");
+ Assert.notNull(queryOptions, "QueryOptions must not be null");
+
+ SimpleStatement statement = new SimpleStatement(cql);
+
+ QueryOptionsUtil.addQueryOptions(statement, queryOptions);
+
+ return new CachedPreparedStatementCreator(cache, statement);
+ }
+
+ /**
+ * @return the underlying {@link PreparedStatementCache}.
+ */
+ public PreparedStatementCache getCache() {
+ return cache;
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.cql.core.PreparedStatementCreator#createPreparedStatement(com.datastax.driver.core.Session)
+ */
+ @Override
+ public PreparedStatement createPreparedStatement(Session session) throws DriverException {
+ return cache.getPreparedStatement(session, statement, () -> session.prepare(statement));
+ }
+}
diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cql/core/support/MapPreparedStatementCache.java b/spring-data-cassandra/src/main/java/org/springframework/data/cql/core/support/MapPreparedStatementCache.java
new file mode 100644
index 000000000..275a13da5
--- /dev/null
+++ b/spring-data-cassandra/src/main/java/org/springframework/data/cql/core/support/MapPreparedStatementCache.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2017 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.data.cql.core.support;
+
+import lombok.EqualsAndHashCode;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Supplier;
+
+import org.springframework.util.Assert;
+
+import com.datastax.driver.core.Cluster;
+import com.datastax.driver.core.PreparedStatement;
+import com.datastax.driver.core.RegularStatement;
+import com.datastax.driver.core.Session;
+
+/**
+ * {@link PreparedStatementCache} backed by a {@link Map} cache. Defaults to simple {@link ConcurrentHashMap} caching.
+ *
+ * Statements are cached with a key consisting of {@link Cluster}, {@code keyspace} and the {@code cql} text. Statement
+ * options (idempotency, timeouts) apply from the statement that was initially prepared.
+ *
+ * @author Mark Paluch
+ * @since 2.0
+ */
+public class MapPreparedStatementCache implements PreparedStatementCache {
+
+ private final Map cache;
+
+ /**
+ * Create a new {@link MapPreparedStatementCache}.
+ *
+ * @param cache must not be {@literal null}.
+ */
+ private MapPreparedStatementCache(Map cache) {
+
+ Assert.notNull(cache, "Cache must not be null");
+
+ this.cache = cache;
+ }
+
+ /**
+ * Create a {@link MapPreparedStatementCache} using {@link ConcurrentHashMap}.
+ *
+ * @return the new {@link MapPreparedStatementCache} backed by {@link ConcurrentHashMap}.
+ */
+ public static MapPreparedStatementCache create() {
+ return of(new ConcurrentHashMap<>());
+ }
+
+ /**
+ * Create a {@link MapPreparedStatementCache} using the given {@link Map}.
+ *
+ * @return the new {@link MapPreparedStatementCache} backed the given {@link Map}.
+ */
+ public static MapPreparedStatementCache of(Map cache) {
+ return new MapPreparedStatementCache(cache);
+ }
+
+ /**
+ * @return the underlying {@link Map cache}.
+ */
+ public Map getCache() {
+ return cache;
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.cql.core.support.PrepatedStatementCache#getPreparedStatement(com.datastax.driver.core.RegularStatement, com.datastax.driver.core.Session, java.util.function.Supplier)
+ */
+ @Override
+ public PreparedStatement getPreparedStatement(Session session, RegularStatement statement,
+ Supplier preparer) {
+
+ CacheKey cacheKey = new CacheKey(session, statement.toString());
+
+ return cache.computeIfAbsent(cacheKey, key -> session.prepare(statement));
+ }
+
+ /**
+ * Cache key for {@link PreparedStatement} caching.
+ */
+ @EqualsAndHashCode
+ public static class CacheKey {
+
+ final Cluster cluster;
+ final String keyspace;
+ final String cql;
+
+ CacheKey(Session session, String cql) {
+
+ this.cluster = session.getCluster();
+ this.keyspace = session.getLoggedKeyspace();
+ this.cql = cql;
+ }
+ }
+}
diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cql/core/support/PreparedStatementCache.java b/spring-data-cassandra/src/main/java/org/springframework/data/cql/core/support/PreparedStatementCache.java
new file mode 100644
index 000000000..0f509b3b6
--- /dev/null
+++ b/spring-data-cassandra/src/main/java/org/springframework/data/cql/core/support/PreparedStatementCache.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2017 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.data.cql.core.support;
+
+import java.util.function.Supplier;
+
+import com.datastax.driver.core.PreparedStatement;
+import com.datastax.driver.core.RegularStatement;
+import com.datastax.driver.core.Session;
+
+/**
+ * Cache interface to synchronously prepare CQL statements.
+ *
+ * Implementing classes of {@link PreparedStatementCache} come with own synchronization and cache implementation
+ * characteristics. A cache implementation should optimize for reduction of preparation calls and cache statements using
+ * Cassandras cache key which is specific to the Cluster, keyspace, and CQL text.
+ *
+ * @author Mark Paluch
+ * @since 2.0
+ * @see PreparedStatement
+ */
+public interface PreparedStatementCache {
+
+ /**
+ * Obtain a {@link PreparedStatement} by {@link Session} and {@link RegularStatement}.
+ *
+ * @param session must not be {@literal null}.
+ * @param statement must not be {@literal null}.
+ * @param preparer must not be {@literal null}.
+ * @return the {@link PreparedStatement}.
+ */
+ PreparedStatement getPreparedStatement(Session session, RegularStatement statement,
+ Supplier preparer);
+
+ /**
+ * Create a default cache backed by a {@link java.util.concurrent.ConcurrentHashMap}.
+ *
+ * @return a new {@link MapPreparedStatementCache}.
+ */
+ static PreparedStatementCache create() {
+ return MapPreparedStatementCache.create();
+ }
+}
diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cql/core/support/CachedPreparedStatementCreatorIntegrationTest.java b/spring-data-cassandra/src/test/java/org/springframework/data/cql/core/support/CachedPreparedStatementCreatorIntegrationTest.java
new file mode 100644
index 000000000..b7fd54849
--- /dev/null
+++ b/spring-data-cassandra/src/test/java/org/springframework/data/cql/core/support/CachedPreparedStatementCreatorIntegrationTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2017 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.data.cql.core.support;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.*;
+import static org.assertj.core.api.Assertions.*;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.data.cql.AbstractKeyspaceCreatingIntegrationTest;
+
+import com.datastax.driver.core.PreparedStatement;
+import com.datastax.driver.core.querybuilder.Insert;
+import com.datastax.driver.core.querybuilder.QueryBuilder;
+
+/**
+ * Integration tests for {@link CachedPreparedStatementCreator}.
+ *
+ * @author Mark Paluch
+ */
+public class CachedPreparedStatementCreatorIntegrationTest extends AbstractKeyspaceCreatingIntegrationTest {
+
+ private static final AtomicBoolean initialized = new AtomicBoolean();
+
+ @Before
+ public void before() throws Exception {
+
+ if (initialized.compareAndSet(false, true)) {
+ getSession().execute("CREATE TABLE IF NOT EXISTS user (id text PRIMARY KEY, username text);");
+ } else {
+ session.execute("TRUNCATE user;");
+ }
+ }
+
+ @Test // DATACASS-403
+ public void shouldRetainIdempotencyFlag() {
+
+ Insert insert = QueryBuilder.insertInto("user").value("id", bindMarker()).value("username", bindMarker());
+
+ assertThat(insert.isIdempotent()).isTrue();
+
+ PreparedStatementCache cache = PreparedStatementCache.create();
+ PreparedStatement preparedStatement = CachedPreparedStatementCreator.of(cache, insert)
+ .createPreparedStatement(session);
+
+ assertThat(preparedStatement.isIdempotent()).isTrue();
+ }
+}
diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cql/core/support/CachedPreparedStatementCreatorUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cql/core/support/CachedPreparedStatementCreatorUnitTests.java
new file mode 100644
index 000000000..14f2bf346
--- /dev/null
+++ b/spring-data-cassandra/src/test/java/org/springframework/data/cql/core/support/CachedPreparedStatementCreatorUnitTests.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2017 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.data.cql.core.support;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import com.datastax.driver.core.Cluster;
+import com.datastax.driver.core.PreparedStatement;
+import com.datastax.driver.core.RegularStatement;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.core.SimpleStatement;
+import com.datastax.driver.core.querybuilder.QueryBuilder;
+
+/**
+ * Unit tests for {@link CachedPreparedStatementCreator}.
+ *
+ * @author Mark Paluch
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class CachedPreparedStatementCreatorUnitTests {
+
+ @Mock Session session;
+
+ @Mock Session otherSession;
+
+ @Mock Session otherKeyspaceSession;
+
+ @Mock Cluster cluster;
+
+ @Mock PreparedStatement preparedStatement;
+
+ @Before
+ public void before() {
+
+ when(session.getCluster()).thenReturn(cluster);
+ when(otherSession.getCluster()).thenReturn(cluster);
+ when(otherKeyspaceSession.getCluster()).thenReturn(cluster);
+
+ when(session.getLoggedKeyspace()).thenReturn("keyspace");
+ when(otherSession.getLoggedKeyspace()).thenReturn("keyspace");
+ when(otherKeyspaceSession.getLoggedKeyspace()).thenReturn("other");
+
+ when(session.prepare(any(RegularStatement.class))).thenReturn(preparedStatement);
+ }
+
+ @Test // DATACASS-403
+ public void shouldPrepareStatement() {
+
+ String cql = "SELECT foo FROM users;";
+
+ MapPreparedStatementCache cache = MapPreparedStatementCache.create();
+
+ CachedPreparedStatementCreator creator = CachedPreparedStatementCreator.of(cache, cql);
+
+ PreparedStatement result = creator.createPreparedStatement(session);
+
+ assertThat(result).isSameAs(preparedStatement);
+ assertThat(creator.getCache()).isSameAs(cache);
+ assertThat(cache.getCache()).hasSize(1);
+ }
+
+ @Test // DATACASS-403
+ public void shouldCachePreparedStatement() {
+
+ String cql = "SELECT foo FROM users;";
+
+ PreparedStatementCache cache = PreparedStatementCache.create();
+
+ assertThat(CachedPreparedStatementCreator.of(cache, cql).createPreparedStatement(session))
+ .isSameAs(preparedStatement);
+ assertThat(CachedPreparedStatementCreator.of(cache, cql).createPreparedStatement(session))
+ .isSameAs(preparedStatement);
+
+ verify(session, atMost(1)).prepare(any(SimpleStatement.class));
+ }
+
+ @Test // DATACASS-403
+ public void shouldCachePreparedStatementAcrossSessions() {
+
+ String cql = "SELECT foo FROM users;";
+
+ PreparedStatementCache cache = PreparedStatementCache.create();
+
+ CachedPreparedStatementCreator creator = CachedPreparedStatementCreator.of(cache, cql);
+
+ assertThat(creator.createPreparedStatement(session)).isSameAs(preparedStatement);
+ assertThat(creator.createPreparedStatement(otherSession)).isSameAs(preparedStatement);
+
+ verify(session, atMost(1)).prepare(any(SimpleStatement.class));
+ verify(otherSession, never()).prepare(any(SimpleStatement.class));
+ }
+
+ @Test // DATACASS-403
+ public void shouldCachePreparedStatementOnKeyspaceLevel() {
+
+ String cql = "SELECT foo FROM users;";
+ when(otherKeyspaceSession.prepare(any(RegularStatement.class))).thenReturn(preparedStatement);
+
+ PreparedStatementCache cache = PreparedStatementCache.create();
+
+ CachedPreparedStatementCreator creator = CachedPreparedStatementCreator.of(cache, cql);
+
+ assertThat(creator.createPreparedStatement(session)).isSameAs(preparedStatement);
+ assertThat(creator.createPreparedStatement(otherKeyspaceSession)).isSameAs(preparedStatement);
+
+ verify(session).prepare(any(SimpleStatement.class));
+ verify(otherKeyspaceSession).prepare(any(SimpleStatement.class));
+ }
+
+ @Test // DATACASS-403
+ public void shouldCacheBuiltPreparedStatement() {
+
+ RegularStatement statement = QueryBuilder.update("users").with(QueryBuilder.set("foo", "bar"));
+
+ PreparedStatementCache cache = PreparedStatementCache.create();
+
+ when(session.prepare(statement)).thenReturn(preparedStatement);
+
+ assertThat(CachedPreparedStatementCreator.of(cache, statement).createPreparedStatement(session))
+ .isSameAs(preparedStatement);
+ assertThat(CachedPreparedStatementCreator.of(cache, statement).createPreparedStatement(session))
+ .isSameAs(preparedStatement);
+
+ assertThat(statement.isIdempotent()).isTrue();
+ verify(session).prepare(statement);
+ }
+
+ @Test // DATACASS-403
+ public void shouldCacheSameBuiltPreparedStatements() {
+
+ RegularStatement firstStatement = QueryBuilder.update("users").with(QueryBuilder.set("foo", "bar"));
+ RegularStatement secondStatement = QueryBuilder.update("users").with(QueryBuilder.set("foo", "bar"));
+
+ PreparedStatementCache cache = PreparedStatementCache.create();
+
+ when(session.prepare(firstStatement)).thenReturn(preparedStatement);
+
+ CachedPreparedStatementCreator.of(cache, firstStatement).createPreparedStatement(session);
+ CachedPreparedStatementCreator.of(cache, secondStatement).createPreparedStatement(session);
+
+ verify(session).prepare(firstStatement);
+ }
+
+ @Test // DATACASS-403
+ public void shouldCacheAdoptDifferencesInCachedPreparedStatements() {
+
+ RegularStatement firstStatement = QueryBuilder.update("users").with(QueryBuilder.set("foo", "bar"));
+ RegularStatement secondStatement = QueryBuilder.update("users").with(QueryBuilder.set("bar", "foo"));
+
+ PreparedStatementCache cache = PreparedStatementCache.create();
+
+ when(session.prepare(firstStatement)).thenReturn(preparedStatement);
+ when(session.prepare(secondStatement)).thenReturn(preparedStatement);
+
+ CachedPreparedStatementCreator.of(cache, firstStatement).createPreparedStatement(session);
+ CachedPreparedStatementCreator.of(cache, secondStatement).createPreparedStatement(session);
+
+ verify(session).prepare(firstStatement);
+ verify(session).prepare(secondStatement);
+ }
+}