+ * Implementations must define a no-args constructor.
+ *
+ * @author Chris Bono
+ * @since 5.0
+ */
+public interface CassandraRepositoryFragmentsContributor extends RepositoryFragmentsContributor {
+
+ CassandraRepositoryFragmentsContributor DEFAULT = EmptyFragmentsContributor.INSTANCE;
+
+ /**
+ * Returns a composed {@code CassandraRepositoryFragmentsContributor} that first applies this contributor to its inputs,
+ * and then applies the {@code after} contributor concatenating effectively both results. If evaluation of either
+ * contributors throws an exception, it is relayed to the caller of the composed contributor.
+ *
+ * @param after the contributor to apply after this contributor is applied.
+ * @return a composed contributor that first applies this contributor and then applies the {@code after} contributor.
+ */
+ default CassandraRepositoryFragmentsContributor andThen(CassandraRepositoryFragmentsContributor after) {
+
+ Assert.notNull(after, "CassandraRepositoryFragmentsContributor must not be null");
+
+ return new CassandraRepositoryFragmentsContributor() {
+
+ @Override
+ public RepositoryFragments contribute(RepositoryMetadata metadata,
+ CassandraEntityInformation, ?> entityInformation, CassandraOperations operations) {
+ return CassandraRepositoryFragmentsContributor.this.contribute(metadata, entityInformation, operations)
+ .append(after.contribute(metadata, entityInformation, operations));
+ }
+
+ @Override
+ public RepositoryFragments describe(RepositoryMetadata metadata) {
+ return CassandraRepositoryFragmentsContributor.this.describe(metadata).append(after.describe(metadata));
+ }
+ };
+ }
+
+ /**
+ * Creates {@link RepositoryFragments} based on {@link RepositoryMetadata} to add
+ * Cassandra-specific extensions.
+ *
+ * @param metadata repository metadata.
+ * @param entityInformation must not be {@literal null}.
+ * @param operations must not be {@literal null}.
+ * @return {@link RepositoryFragments} to be added to the repository.
+ */
+ RepositoryFragments contribute(RepositoryMetadata metadata,
+ CassandraEntityInformation, ?> entityInformation, CassandraOperations operations);
+
+}
diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/EmptyFragmentsContributor.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/EmptyFragmentsContributor.java
new file mode 100644
index 000000000..3bf69f572
--- /dev/null
+++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/EmptyFragmentsContributor.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 org.springframework.data.cassandra.repository.support;
+
+import org.springframework.data.cassandra.core.CassandraOperations;
+import org.springframework.data.cassandra.repository.query.CassandraEntityInformation;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.core.support.RepositoryComposition;
+
+/**
+ * Implementation of {@link CassandraRepositoryFragmentsContributor} that contributes empty fragments by default.
+ *
+ * @author Chris Bono
+ * @since 5.0
+ */
+enum EmptyFragmentsContributor implements CassandraRepositoryFragmentsContributor {
+
+ INSTANCE;
+
+ @Override
+ public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata,
+ CassandraEntityInformation, ?> entityInformation, CassandraOperations operations) {
+ return RepositoryComposition.RepositoryFragments.empty();
+ }
+
+ @Override
+ public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) {
+ return RepositoryComposition.RepositoryFragments.empty();
+ }
+}
diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveCassandraRepositoryFactory.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveCassandraRepositoryFactory.java
index 65dc0f6d1..fea55f9d6 100644
--- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveCassandraRepositoryFactory.java
+++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveCassandraRepositoryFactory.java
@@ -34,6 +34,7 @@
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport;
+import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments;
import org.springframework.data.repository.query.CachingValueExpressionDelegate;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
@@ -46,6 +47,7 @@
*
* @author Mark Paluch
* @author Marcin Grzejszczak
+ * @author Chris Bono
* @since 2.0
*/
public class ReactiveCassandraRepositoryFactory extends ReactiveRepositoryFactorySupport {
@@ -54,6 +56,8 @@ public class ReactiveCassandraRepositoryFactory extends ReactiveRepositoryFactor
private final MappingContext extends CassandraPersistentEntity>, ? extends CassandraPersistentProperty> mappingContext;
+ private ReactiveCassandraRepositoryFragmentsContributor fragmentsContributor = ReactiveCassandraRepositoryFragmentsContributor.DEFAULT;
+
/**
* Create a new {@link ReactiveCassandraRepositoryFactory} with the given {@link ReactiveCassandraOperations}.
*
@@ -67,6 +71,17 @@ public ReactiveCassandraRepositoryFactory(ReactiveCassandraOperations cassandraO
this.mappingContext = cassandraOperations.getConverter().getMappingContext();
}
+ /**
+ * Configures the {@link ReactiveCassandraRepositoryFragmentsContributor} to be used. Defaults to
+ * {@link ReactiveCassandraRepositoryFragmentsContributor#DEFAULT}.
+ *
+ * @param fragmentsContributor
+ * @since 5.0
+ */
+ public void setFragmentsContributor(ReactiveCassandraRepositoryFragmentsContributor fragmentsContributor) {
+ this.fragmentsContributor = fragmentsContributor;
+ }
+
@Override
protected ProjectionFactory getProjectionFactory(@Nullable ClassLoader classLoader,
@Nullable BeanFactory beanFactory) {
@@ -101,6 +116,24 @@ public
+ * Implementations must define a no-args constructor.
+ *
+ * @author Chris Bono
+ * @since 5.0
+ */
+public interface ReactiveCassandraRepositoryFragmentsContributor extends RepositoryFragmentsContributor {
+
+ ReactiveCassandraRepositoryFragmentsContributor DEFAULT = ReactiveEmptyFragmentsContributor.INSTANCE;
+
+ /**
+ * Returns a composed {@code ReactiveCassandraRepositoryFragmentsContributor} that first applies this contributor to
+ * its inputs, and then applies the {@code after} contributor concatenating effectively both results. If evaluation
+ * of either contributors throws an exception, it is relayed to the caller of the composed contributor.
+ *
+ * @param after the contributor to apply after this contributor is applied.
+ * @return a composed contributor that first applies this contributor and then applies the {@code after} contributor.
+ */
+ default ReactiveCassandraRepositoryFragmentsContributor andThen(ReactiveCassandraRepositoryFragmentsContributor after) {
+
+ Assert.notNull(after, "ReactiveCassandraRepositoryFragmentsContributor must not be null");
+
+ return new ReactiveCassandraRepositoryFragmentsContributor() {
+
+ @Override
+ public RepositoryFragments contribute(RepositoryMetadata metadata,
+ CassandraEntityInformation, ?> entityInformation, ReactiveCassandraOperations operations) {
+ return ReactiveCassandraRepositoryFragmentsContributor.this.contribute(metadata, entityInformation, operations)
+ .append(after.contribute(metadata, entityInformation, operations));
+ }
+
+ @Override
+ public RepositoryFragments describe(RepositoryMetadata metadata) {
+ return ReactiveCassandraRepositoryFragmentsContributor.this.describe(metadata).append(after.describe(metadata));
+ }
+ };
+ }
+
+ /**
+ * Creates {@link RepositoryFragments} based on {@link RepositoryMetadata} to add
+ * Cassandra-specific extensions.
+ *
+ * @param metadata repository metadata.
+ * @param entityInformation must not be {@literal null}.
+ * @param operations must not be {@literal null}.
+ * @return {@link RepositoryFragments} to be added to the repository.
+ */
+ RepositoryFragments contribute(RepositoryMetadata metadata,
+ CassandraEntityInformation, ?> entityInformation, ReactiveCassandraOperations operations);
+
+}
diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveEmptyFragmentsContributor.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveEmptyFragmentsContributor.java
new file mode 100644
index 000000000..76426b560
--- /dev/null
+++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveEmptyFragmentsContributor.java
@@ -0,0 +1,44 @@
+/*
+ * 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 org.springframework.data.cassandra.repository.support;
+
+import org.springframework.data.cassandra.core.ReactiveCassandraOperations;
+import org.springframework.data.cassandra.repository.query.CassandraEntityInformation;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.core.support.RepositoryComposition;
+
+/**
+ * Implementation of {@link ReactiveCassandraRepositoryFragmentsContributor} that contributes empty fragments by
+ * default.
+ *
+ * @author Chris Bono
+ * @since 5.0
+ */
+enum ReactiveEmptyFragmentsContributor implements ReactiveCassandraRepositoryFragmentsContributor {
+
+ INSTANCE;
+
+ @Override
+ public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata,
+ CassandraEntityInformation, ?> entityInformation, ReactiveCassandraOperations operations) {
+ return RepositoryComposition.RepositoryFragments.empty();
+ }
+
+ @Override
+ public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) {
+ return RepositoryComposition.RepositoryFragments.empty();
+ }
+}
diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/support/CassandraRepositoryFragmentsContributorUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/support/CassandraRepositoryFragmentsContributorUnitTests.java
new file mode 100644
index 000000000..2a457d37e
--- /dev/null
+++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/support/CassandraRepositoryFragmentsContributorUnitTests.java
@@ -0,0 +1,107 @@
+/*
+ * 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 org.springframework.data.cassandra.support;
+
+import java.util.Optional;
+import org.junit.jupiter.api.Test;
+import org.springframework.data.cassandra.core.CassandraOperations;
+import org.springframework.data.cassandra.core.convert.MappingCassandraConverter;
+import org.springframework.data.cassandra.core.mapping.CassandraMappingContext;
+import org.springframework.data.cassandra.domain.User;
+import org.springframework.data.cassandra.repository.query.CassandraEntityInformation;
+import org.springframework.data.cassandra.repository.support.CassandraRepositoryFragmentsContributor;
+import org.springframework.data.cassandra.repository.support.MappingCassandraEntityInformation;
+import org.springframework.data.repository.Repository;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.core.support.AbstractRepositoryMetadata;
+import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments;
+import org.springframework.data.repository.core.support.RepositoryFragment;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * Unit tests for {@link CassandraRepositoryFragmentsContributor}.
+ *
+ * @author Chris Bono
+ */
+class CassandraRepositoryFragmentsContributorUnitTests {
+
+ @Test // GH-3279
+ void composedContributorShouldCreateFragments() {
+
+ var mappingContext = new CassandraMappingContext();
+ var converter = new MappingCassandraConverter(mappingContext);
+ CassandraOperations operations = mock();
+ when(operations.getConverter()).thenReturn(converter);
+
+ var contributor = CassandraRepositoryFragmentsContributor.DEFAULT
+ .andThen(MyCassandraRepositoryFragmentsContributor.INSTANCE)
+ .andThen(MyOtherCassandraRepositoryFragmentsContributor.INSTANCE);
+
+ var fragments = contributor.contribute(
+ AbstractRepositoryMetadata.getMetadata(MyUserRepo.class),
+ new MappingCassandraEntityInformation<>(mappingContext.getPersistentEntity(User.class), converter),
+ operations);
+
+ assertThat(fragments.stream())
+ .extracting(RepositoryFragment::getImplementationClass)
+ .containsExactly(Optional.of(MyFragment.class), Optional.of(MyOtherFragment.class));
+ }
+
+ enum MyCassandraRepositoryFragmentsContributor implements CassandraRepositoryFragmentsContributor {
+
+ INSTANCE;
+
+ @Override
+ public RepositoryFragments contribute(RepositoryMetadata metadata,
+ CassandraEntityInformation, ?> entityInformation, CassandraOperations operations) {
+ return RepositoryFragments.just(new MyFragment());
+ }
+
+ @Override
+ public RepositoryFragments describe(RepositoryMetadata metadata) {
+ return RepositoryFragments.just(new MyFragment());
+ }
+ }
+
+ enum MyOtherCassandraRepositoryFragmentsContributor implements CassandraRepositoryFragmentsContributor {
+
+ INSTANCE;
+
+ @Override
+ public RepositoryFragments contribute(RepositoryMetadata metadata,
+ CassandraEntityInformation, ?> entityInformation, CassandraOperations operations) {
+ return RepositoryFragments.just(new MyOtherFragment());
+ }
+
+ @Override
+ public RepositoryFragments describe(RepositoryMetadata metadata) {
+ return RepositoryFragments.just(new MyOtherFragment());
+ }
+ }
+
+ static class MyFragment {
+
+ }
+
+ static class MyOtherFragment {
+
+ }
+
+ interface MyUserRepo extends Repository