diff --git a/README.md b/README.md
index 86f8090c..b6671009 100644
--- a/README.md
+++ b/README.md
@@ -44,6 +44,8 @@ Local Elasticsearch instance must be running to run the tests.
## Spring Data JDBC
* `basic` - Basic usage of Spring Data JDBC.
+* `immutables` - Showing Spring Data JDBC usage
+ with [Immutables](https://immutables.github.io/)
## Spring Data JPA
diff --git a/jdbc/immutables/README.adoc b/jdbc/immutables/README.adoc
new file mode 100644
index 00000000..3553ccc2
--- /dev/null
+++ b/jdbc/immutables/README.adoc
@@ -0,0 +1,27 @@
+== Spring Data JDBC with Immutables
+
+This example show how to use https://immutables.github.io/[Immutables] with Spring Data.
+The core concept of Immutables is to define an interface (or abstract class) for which Immutables generates an immutable implementation that can then be used by application code.
+
+Persisting immutable objects and associating the saved object with generated identifiers works out of the box.
+The reading side requires a redirection of the to be created object type, see `ImmutablesJdbcConfiguration`.
+
+Limitations:
+
+Immutables tends to generate additional constructors when using `@Value.Style(allParameters = true)` or `@Value.Parameter`.
+This conflicts with Spring Data's constructor resolution as Spring Data cannot identify reliably a persistence constructor.
+
+To run the tests, run:
+
+[indent=0]
+----
+ $ mvn test
+----
+
+The code generator is automatically run when executing the tests.
+If you want to rerun the code generator manually, just run the following command:
+
+[indent=0]
+----
+ $ mvn clean generate-sources
+----
diff --git a/jdbc/immutables/pom.xml b/jdbc/immutables/pom.xml
new file mode 100644
index 00000000..8527b31d
--- /dev/null
+++ b/jdbc/immutables/pom.xml
@@ -0,0 +1,28 @@
+
+ 4.0.0
+
+ spring-data-jdbc-immutables
+
+
+ org.springframework.data.examples
+ spring-data-jdbc-examples
+ 2.0.0.BUILD-SNAPSHOT
+ ../pom.xml
+
+
+ Spring Data JDBC - Usage with Immutables
+ Sample project demonstrating Spring Data JDBC features
+
+
+
+ org.immutables
+ value
+ 2.8.8
+ provided
+
+
+
+
+
diff --git a/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Application.java b/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Application.java
new file mode 100644
index 00000000..073c3c3b
--- /dev/null
+++ b/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Application.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2021 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 example.springdata.jdbc.immutables;
+
+import java.sql.ResultSet;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.data.jdbc.core.convert.BasicJdbcConverter;
+import org.springframework.data.jdbc.core.convert.DefaultJdbcTypeFactory;
+import org.springframework.data.jdbc.core.convert.Identifier;
+import org.springframework.data.jdbc.core.convert.JdbcConverter;
+import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;
+import org.springframework.data.jdbc.core.convert.RelationResolver;
+import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
+import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration;
+import org.springframework.data.mapping.PersistentPropertyPath;
+import org.springframework.data.mapping.context.MappingContext;
+import org.springframework.data.relational.core.dialect.Dialect;
+import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension;
+import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
+import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
+import org.springframework.util.ClassUtils;
+
+/**
+ * Configuration stub.
+ *
+ * @author Mark Paluch
+ */
+@SpringBootApplication
+class Application {
+
+ /**
+ * Name scheme how Immutables generates implementations from interface/class definitions.
+ */
+ public static final String IMMUTABLE_IMPLEMENTATION_CLASS = "%s.Immutable%s";
+
+ @Configuration
+ static class ImmutablesJdbcConfiguration extends AbstractJdbcConfiguration {
+
+ private final ResourceLoader resourceLoader;
+
+ public ImmutablesJdbcConfiguration(ResourceLoader resourceLoader) {
+ this.resourceLoader = resourceLoader;
+ }
+
+ /**
+ * {@link JdbcConverter} that redirects entities to be instantiated towards the implementation. See
+ * {@link #IMMUTABLE_IMPLEMENTATION_CLASS} and
+ * {@link #getImplementationEntity(JdbcMappingContext, RelationalPersistentEntity)}.
+ *
+ * @param mappingContext
+ * @param operations
+ * @param relationResolver
+ * @param conversions
+ * @param dialect
+ * @return
+ */
+ @Override
+ public JdbcConverter jdbcConverter(JdbcMappingContext mappingContext, NamedParameterJdbcOperations operations,
+ RelationResolver relationResolver, JdbcCustomConversions conversions, Dialect dialect) {
+
+ var jdbcTypeFactory = new DefaultJdbcTypeFactory(operations.getJdbcOperations());
+
+ return new BasicJdbcConverter(mappingContext, relationResolver, conversions, jdbcTypeFactory,
+ dialect.getIdentifierProcessing()) {
+
+ @Override
+ public T mapRow(RelationalPersistentEntity entity, ResultSet resultSet, Object key) {
+ return super.mapRow(getImplementationEntity(mappingContext, entity), resultSet, key);
+ }
+
+ @Override
+ public T mapRow(PersistentPropertyPathExtension path, ResultSet resultSet, Identifier identifier,
+ Object key) {
+
+ return super.mapRow(new DelegatePersistentPropertyPathExtension(mappingContext,
+ path.getRequiredPersistentPropertyPath(), getImplementationEntity(mappingContext, path.getLeafEntity())),
+ resultSet, identifier, key);
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ private RelationalPersistentEntity getImplementationEntity(JdbcMappingContext mappingContext,
+ RelationalPersistentEntity entity) {
+
+ var type = entity.getType();
+ if (type.isInterface()) {
+
+ var immutableClass = String.format(IMMUTABLE_IMPLEMENTATION_CLASS, type.getPackageName(), type.getSimpleName());
+ if (ClassUtils.isPresent(immutableClass, resourceLoader.getClassLoader())) {
+
+ return (RelationalPersistentEntity) mappingContext
+ .getPersistentEntity(ClassUtils.resolveClassName(immutableClass, resourceLoader.getClassLoader()));
+ }
+
+ }
+ return entity;
+ }
+ }
+
+ /**
+ * Redirect {@link #getLeafEntity()} to a different entity type.
+ */
+ static class DelegatePersistentPropertyPathExtension extends PersistentPropertyPathExtension {
+
+ private final RelationalPersistentEntity> leafEntity;
+
+ public DelegatePersistentPropertyPathExtension(
+ MappingContext extends RelationalPersistentEntity>, ? extends RelationalPersistentProperty> context,
+ PersistentPropertyPath extends RelationalPersistentProperty> path, RelationalPersistentEntity> leafEntity) {
+ super(context, path);
+ this.leafEntity = leafEntity;
+ }
+
+ @Override
+ public RelationalPersistentEntity> getLeafEntity() {
+ return leafEntity;
+ }
+ }
+
+}
diff --git a/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Enigma.java b/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Enigma.java
new file mode 100644
index 00000000..f927fb1a
--- /dev/null
+++ b/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Enigma.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2021 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 example.springdata.jdbc.immutables;
+
+import java.util.List;
+
+import org.immutables.value.Value;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.relational.core.mapping.MappedCollection;
+import org.springframework.data.relational.core.mapping.Table;
+import org.springframework.lang.Nullable;
+
+/**
+ * @author Mark Paluch
+ */
+@Value.Immutable
+@Table("ENIGMA")
+public interface Enigma {
+
+ @Nullable
+ @Id
+ Long getId();
+
+ String getModel();
+
+ // Explicit keys to not derive key names from the implementation class.
+ @MappedCollection(idColumn = "ENIGMA_ID", keyColumn = "ROTOR_KEY")
+ List getRotors();
+
+}
diff --git a/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/EnigmaRepository.java b/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/EnigmaRepository.java
new file mode 100644
index 00000000..51611fe4
--- /dev/null
+++ b/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/EnigmaRepository.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2021 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 example.springdata.jdbc.immutables;
+
+import org.springframework.data.repository.CrudRepository;
+
+/**
+ * Repository for {@link Enigma} instances.
+ *
+ * @author Mark Paluch
+ */
+public interface EnigmaRepository extends CrudRepository {
+
+ Enigma findByModel(String name);
+}
diff --git a/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Rotor.java b/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Rotor.java
new file mode 100644
index 00000000..6b9a25b5
--- /dev/null
+++ b/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Rotor.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2021 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 example.springdata.jdbc.immutables;
+
+import org.immutables.value.Value;
+
+import org.springframework.data.relational.core.mapping.Table;
+
+/**
+ * @author Mark Paluch
+ */
+@Value.Immutable
+@Table("ROTOR")
+public interface Rotor {
+
+ String getName();
+
+ String getWiring();
+
+ char getNotch();
+
+ /**
+ * Factory method for {@link Rotor} as using {@code @Value.Style} and {@code @Value.Parameter} conflicts with Spring
+ * Data's constructor discovery rules.
+ *
+ * @param name
+ * @param wiring
+ * @param notch
+ * @return
+ */
+ static Rotor of(String name, String wiring, char notch) {
+ return ImmutableRotor.builder().name(name).wiring(wiring).notch(notch).build();
+ }
+
+}
diff --git a/jdbc/immutables/src/main/resources/schema.sql b/jdbc/immutables/src/main/resources/schema.sql
new file mode 100644
index 00000000..1b397e27
--- /dev/null
+++ b/jdbc/immutables/src/main/resources/schema.sql
@@ -0,0 +1,34 @@
+CREATE TABLE IF NOT EXISTS ENIGMA
+(
+ ID
+ INTEGER
+ IDENTITY
+ PRIMARY
+ KEY,
+ MODEL
+ VARCHAR
+(
+ 100
+)
+ );
+
+CREATE TABLE IF NOT EXISTS ROTOR
+(
+ ENIGMA_ID
+ INTEGER,
+ ROTOR_KEY
+ INTEGER,
+ NAME
+ VARCHAR
+(
+ 100
+),
+ WIRING VARCHAR
+(
+ 26
+),
+ NOTCH CHAR
+(
+ 1
+)
+ );
diff --git a/jdbc/immutables/src/test/java/example/springdata/jdbc/immutables/ImmutablesTests.java b/jdbc/immutables/src/test/java/example/springdata/jdbc/immutables/ImmutablesTests.java
new file mode 100644
index 00000000..be11962c
--- /dev/null
+++ b/jdbc/immutables/src/test/java/example/springdata/jdbc/immutables/ImmutablesTests.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2021 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 example.springdata.jdbc.immutables;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest;
+
+/**
+ * Integration tests using immutable types assisted by Immutables. Calling code can use the actual immutable
+ * interface/abstract class while Spring Data materializes the actual implementation.
+ *
+ * @author Mark Paluch
+ * @see https://immutables.github.io/
+ */
+@DataJdbcTest
+class ImmutablesTests {
+
+ @Autowired EnigmaRepository repository;
+
+ @Test
+ void shouldInsertAndRetrieveObject() {
+
+ var enigmaA = ImmutableEnigma.builder().model("A") //
+ .addRotors(Rotor.of("I", "DMTWSILRUYQNKFEJCAZBPGXOHV", 'Q')) //
+ .addRotors(Rotor.of("II", "HQZGPJTMOBLNCIFDYAWVEUSRXL", 'E')) //
+ .build();
+
+ var saved = repository.save(enigmaA);
+
+ assertThat(saved.getId()).isNotNull();
+
+ var loaded = repository.findByModel("A");
+
+ assertThat(loaded).isEqualTo(saved).isInstanceOf(ImmutableEnigma.class);
+ assertThat(loaded.getRotors()).hasSize(2);
+ assertThat(loaded.getRotors().get(0)).isInstanceOf(ImmutableRotor.class);
+ }
+}
diff --git a/jdbc/pom.xml b/jdbc/pom.xml
index f7a9dada..4cb80a13 100644
--- a/jdbc/pom.xml
+++ b/jdbc/pom.xml
@@ -19,6 +19,7 @@
basics
mybatis
+ immutables
jmolecules
jooq