diff --git a/jdbc/composite-ids/README.adoc b/jdbc/composite-ids/README.adoc
new file mode 100644
index 000000000..739d1d369
--- /dev/null
+++ b/jdbc/composite-ids/README.adoc
@@ -0,0 +1,8 @@
+== Spring Data JDBC Composite Id
+
+=== EmployeeTest
+
+Demonstrates saving an entity with composite id.
+
+Once by using a direct insert, via a custom `insert` method in the repository, backed by `JdbcAggregateTemplate.insert` and once by a custom id generating callback.
+See `CompositeConfiguration.idGeneration()`.
\ No newline at end of file
diff --git a/jdbc/composite-ids/pom.xml b/jdbc/composite-ids/pom.xml
new file mode 100644
index 000000000..b201ee2e6
--- /dev/null
+++ b/jdbc/composite-ids/pom.xml
@@ -0,0 +1,35 @@
+
+ 4.0.0
+
+ spring-data-jdbc-composite-ids
+
+
+ org.springframework.data.examples
+ spring-data-jdbc-examples
+ 2.0.0.BUILD-SNAPSHOT
+ ../pom.xml
+
+
+ Spring Data JDBC - Examples using composite ids
+ Sample project demonstrating Spring Data JDBCs support for custom ids
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.data
+ spring-data-jdbc
+ 4.0.0-SNAPSHOT
+
+
+ org.springframework.data
+ spring-data-relational
+ 4.0.0-SNAPSHOT
+
+
+
+
diff --git a/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/CompositeConfiguration.java b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/CompositeConfiguration.java
new file mode 100644
index 000000000..776d4a65a
--- /dev/null
+++ b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/CompositeConfiguration.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2025 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
+ *
+ * https://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.compositeid;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration;
+import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
+import org.springframework.data.relational.core.mapping.event.BeforeConvertCallback;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+
+/**
+ * Configuration for the demonstration of composite ids.
+ *
+ * Registers a {@link BeforeConvertCallback} for generating ids.
+ *
+ * @author Jens Schauder
+ */
+@Configuration
+@EnableJdbcRepositories
+public class CompositeConfiguration extends AbstractJdbcConfiguration {
+
+ @Bean
+ BeforeConvertCallback idGeneration() {
+ return new BeforeConvertCallback<>() {
+ AtomicLong counter = new AtomicLong();
+
+ @Override
+ public Employee onBeforeConvert(Employee employee) {
+ if (employee.id == null) {
+ employee.id = new EmployeeId(Organization.RND, counter.addAndGet(1));
+ }
+ return employee;
+ }
+ };
+ }
+}
diff --git a/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/Employee.java b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/Employee.java
new file mode 100644
index 000000000..070614f6e
--- /dev/null
+++ b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/Employee.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2025 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
+ *
+ * https://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.compositeid;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.PersistenceCreator;
+
+/**
+ * A simple entity sporting a compostite id.
+ *
+ * @author Jens Schauder
+ */
+class Employee {
+
+ @Id
+ EmployeeId id;
+
+ String name;
+
+ @PersistenceCreator
+ Employee(EmployeeId id, String name) {
+
+ this.id = id;
+ this.name = name;
+ }
+
+ Employee(String name) {
+ this.name = name;
+ }
+}
diff --git a/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/EmployeeId.java b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/EmployeeId.java
new file mode 100644
index 000000000..835ae6a62
--- /dev/null
+++ b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/EmployeeId.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2025 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
+ *
+ * https://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.compositeid;
+
+/**
+ * Composite id for {@link Employee} instances.
+ *
+ * @author Jens Schauder
+ */
+record EmployeeId(
+ Organization organization,
+ Long employeeNumber) {
+}
diff --git a/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/EmployeeRepository.java b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/EmployeeRepository.java
new file mode 100644
index 000000000..20138d844
--- /dev/null
+++ b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/EmployeeRepository.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2025 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
+ *
+ * https://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.compositeid;
+
+import org.springframework.data.repository.ListCrudRepository;
+
+
+/**
+ * Repositories for {@link Employee} instances.
+ *
+ * @author Jens Schauder
+ * @see InsertRepository
+ * @see InsertRepositoryImpl
+ */
+interface EmployeeRepository extends ListCrudRepository, InsertRepository {
+}
diff --git a/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/InsertRepository.java b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/InsertRepository.java
new file mode 100644
index 000000000..562b26ffb
--- /dev/null
+++ b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/InsertRepository.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2025 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
+ *
+ * https://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.compositeid;
+
+
+/**
+ * Interface for repositories supporting an {@literal insert} operation, that always performs an insert on the database
+ * and does not check the instance.
+ *
+ * @author Jens Schauder
+ */
+interface InsertRepository {
+ E insert(E employee);
+}
diff --git a/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/InsertRepositoryImpl.java b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/InsertRepositoryImpl.java
new file mode 100644
index 000000000..a66d5b601
--- /dev/null
+++ b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/InsertRepositoryImpl.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2025 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
+ *
+ * https://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.compositeid;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.jdbc.core.JdbcAggregateTemplate;
+
+/**
+ * Fragment implementing the {@literal insert} operation using a {@link JdbcAggregateTemplate}.
+ *
+ * @author Jens Schauder
+ */
+class InsertRepositoryImpl implements InsertRepository {
+ @Autowired
+ private JdbcAggregateTemplate template;
+ @Override
+ public E insert(E employee) {
+ return template.insert(employee);
+ }
+}
diff --git a/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/Organization.java b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/Organization.java
new file mode 100644
index 000000000..d53191357
--- /dev/null
+++ b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/Organization.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2025 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
+ *
+ * https://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.compositeid;
+
+/**
+ * Just an enum to be part of the composite id, to demonstrate that one may use various datatypes.
+ *
+ * @author Jens Schauder
+ */
+enum Organization {
+ RND,
+ SALES,
+ MARKETING,
+ PURCHASING
+}
diff --git a/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/package-info.java b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/package-info.java
new file mode 100644
index 000000000..b7ef233b4
--- /dev/null
+++ b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/package-info.java
@@ -0,0 +1,4 @@
+@NonNullApi
+package example.springdata.jdbc.compositeid;
+
+import org.springframework.lang.NonNullApi;
\ No newline at end of file
diff --git a/jdbc/composite-ids/src/main/resources/application.properties b/jdbc/composite-ids/src/main/resources/application.properties
new file mode 100644
index 000000000..2804353df
--- /dev/null
+++ b/jdbc/composite-ids/src/main/resources/application.properties
@@ -0,0 +1,2 @@
+logging.level.org.springframework.data=INFO
+logging.level.org.springframework.jdbc.core.JdbcTemplate=DEBUG
\ No newline at end of file
diff --git a/jdbc/composite-ids/src/main/resources/schema.sql b/jdbc/composite-ids/src/main/resources/schema.sql
new file mode 100644
index 000000000..73e9ee996
--- /dev/null
+++ b/jdbc/composite-ids/src/main/resources/schema.sql
@@ -0,0 +1,6 @@
+create table employee
+(
+ organization varchar(20),
+ employee_number int,
+ name varchar(100)
+);
diff --git a/jdbc/composite-ids/src/test/java/example/springdata/jdbc/compositeid/EmployeeTests.java b/jdbc/composite-ids/src/test/java/example/springdata/jdbc/compositeid/EmployeeTests.java
new file mode 100644
index 000000000..57c291b6e
--- /dev/null
+++ b/jdbc/composite-ids/src/test/java/example/springdata/jdbc/compositeid/EmployeeTests.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2025 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
+ *
+ * https://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.compositeid;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.data.jdbc.AutoConfigureDataJdbc;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import static org.assertj.core.api.Assertions.*;
+
+/**
+ * Test demonstrating the use of composite ids.
+ *
+ * @author Jens Schauder
+ */
+@SpringBootTest(classes = CompositeConfiguration.class)
+@AutoConfigureDataJdbc
+class EmployeeTests {
+
+ @Autowired
+ EmployeeRepository repository;
+
+ @Test
+ void employeeDirectInsert() {
+
+ Employee employee = repository.insert(new Employee(new EmployeeId(Organization.RND, 23L), "Jens Schauder"));
+
+ Employee reloaded = repository.findById(employee.id).orElseThrow();
+
+ assertThat(reloaded.name).isEqualTo(employee.name);
+ }
+
+ @Test
+ void employeeIdGeneration() {
+
+ Employee employee = repository.save(new Employee("Mark Paluch"));
+
+ assertThat(employee.id).isNotNull();
+ }
+}
diff --git a/jdbc/pom.xml b/jdbc/pom.xml
index 81dcfbe55..abbc0043f 100644
--- a/jdbc/pom.xml
+++ b/jdbc/pom.xml
@@ -18,6 +18,7 @@
basics
+ composite-ids
howto
immutables
jmolecules