Spring Data JDBC and R2DBC 4.0 will support Composite IDs

Engineering | Jens Schauder | July 22, 2025 | ...

I'm happy to announce, that Spring Data JDBC and R2DBC finally support Composite IDs starting with version 4.0.0-M4.

Most of you probably know, but just to make sure everyone has the same understanding: From the database point of view a composite id (or composite key) is a primary key that consists of more than one column. On the Java side these columns get mapped to an entity with an attribute for each column Usage should be straight forward and I'll demonstrate it in the following article for JDBC. Usage in R2DBC is anlogous.

To get started just put an @Id annotation on a field in your aggregate root that references an entity, instead of a simple type.

class Employee {

	@Id
	EmployeeId id;

	String name;

	// ...
}

record EmployeeId(
		Organization organization,
		Long employeeNumber) {
}

enum Organization {
	RND,
	SALES,
	MARKETING,
	PURCHASING
}

That reference becomes automatically an embedded reference, i.e. the fields of the entity become columns of the table of the aggregate root.

create table employee
(
    organization    varchar(20),
    employee_number int,
    name            varchar(100)
);

If you want to tweak the names you may add an @Embedded annotation, which allows you to provide a prefix. It does look a little weird to specify what should happen when upon loading the entity is all null values. But with Embedded you have to although a primary key that is null will cause problems all over the place and just won't work.

class Employee {

	@Id
	@Embedded.Nullable(prefix = "id_")
	EmployeeId id;

	String name;
	
	// ... 
}

create table employee
(
    id_organization    varchar(20),
    id_employee_number int,
    name            varchar(100)
);

Just as with normal ids Spring Data Relational uses the the value of id as an indication for new entities: If the id value is null, then the entity is considered new and an insert will be performed. If the id value is not null, an update is in order.

When saving a new entity with composite id you now face a minor problem: Composite Ids can't easily be generated via and autoincrement column, since it by definition consists of multiple columns. One way to handle this is to have a BeforeConvertCallback

@Bean
BeforeConvertCallback<Employee> 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;
		}
	};
}
repository.save(new Employee("Mark Paluch"));

In most cases with composite id it is probably easier to set the id upfront and either use optimistic locking, i.e. a null version field will mark the entity as new, or explicitely call JdbcAggregateTemplate.insert.

interface EmployeeRepository extends Repository<Employee, EmployeeId>, InsertRepository<Employee> {
	Employee findById(EmployeeId id);

	Employee save(Employee employee);
}
interface InsertRepository<E> {
	E insert(E employee);
}
class InsertRepositoryImpl<E> implements InsertRepository<E> {
	@Autowired
	private JdbcAggregateTemplate template;
	@Override
	public E insert(E employee) {
		return template.insert(employee);
	}
}
@Autowired
EmployeeRepository repository;

// ...

repository.insert(new Employee(new EmployeeId(Organization.RND, 23L), "Jens Schauder"));

I hope you find this new addition to Spring Data Relational useful. The full code for the samples used in this article can be found at https://github.com/spring-projects/spring-data-examples/tree/main/jdbc/composite-ids.

If you find bugs or have ideas for improvements, please create a ticket at https://github.com/spring-projects/spring-data-relational/issues.

Get the Spring newsletter

Stay connected with the Spring newsletter

Subscribe

Get ahead

VMware offers training and certification to turbo-charge your progress.

Learn more

Get support

Tanzu Spring offers support and binaries for OpenJDK™, Spring, and Apache Tomcat® in one simple subscription.

Learn more

Upcoming events

Check out all the upcoming events in the Spring community.

View all