, SearchExtension
 
 }
 ----
-====
 
 [[repositories.customize-base-repository]]
 == Customize the Base Repository
diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java
index 88965a4abe..0b73d66a0f 100644
--- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java
+++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java
@@ -127,10 +127,16 @@ public RepositoryFactorySupport() {
 	 * retrieval via the {@code RepositoryMethodContext} class. This is useful if an advised object needs to obtain
 	 * repository information.
 	 * 
-	 * Default is {@literal "false"}, in order to avoid unnecessary extra interception. This means that no guarantees are provided
-	 * that {@code RepositoryMethodContext} access will work consistently within any method of the advised object.
-	 * 
-	 * @since 3.4.0
+	 * Default is {@literal "false"}, in order to avoid unnecessary extra interception. This means that no guarantees are
+	 * provided that {@code RepositoryMethodContext} access will work consistently within any method of the advised
+	 * object.
+	 * 
+	 * Repository method metadata is also exposed if implementations within the {@link RepositoryFragments repository
+	 * composition} implement {@link RepositoryMetadataAccess}.
+	 *
+	 * @since 3.4
+	 * @see RepositoryMethodContext
+	 * @see RepositoryMetadataAccess
 	 */
 	public void setExposeMetadata(boolean exposeMetadata) {
 		this.exposeMetadata = exposeMetadata;
@@ -342,10 +348,16 @@ public  T getRepository(Class repositoryInterface, RepositoryFragments fra
 		result.setInterfaces(repositoryInterface, Repository.class, TransactionalProxy.class);
 
 		if (MethodInvocationValidator.supports(repositoryInterface)) {
+			if (logger.isTraceEnabled()) {
+				logger.trace(LogMessage.format("Register MethodInvocationValidator for %s…", repositoryInterface.getName()));
+			}
 			result.addAdvice(new MethodInvocationValidator());
 		}
 
-		if (this.exposeMetadata) {
+		if (this.exposeMetadata || shouldExposeMetadata(fragments)) {
+			if (logger.isTraceEnabled()) {
+				logger.trace(LogMessage.format("Register ExposeMetadataInterceptor for %s…", repositoryInterface.getName()));
+			}
 			result.addAdvice(new ExposeMetadataInterceptor(metadata));
 			result.addAdvisor(ExposeInvocationInterceptor.ADVISOR);
 		}
@@ -365,6 +377,9 @@ public  T getRepository(Class repositoryInterface, RepositoryFragments fra
 		}
 
 		if (DefaultMethodInvokingMethodInterceptor.hasDefaultMethods(repositoryInterface)) {
+			if (logger.isTraceEnabled()) {
+				logger.trace(LogMessage.format("Register DefaultMethodInvokingMethodInterceptor for %s…", repositoryInterface.getName()));
+			}
 			result.addAdvice(new DefaultMethodInvokingMethodInterceptor());
 		}
 
@@ -616,6 +631,23 @@ private Lazy createProjectionFactory() {
 		return Lazy.of(() -> getProjectionFactory(this.classLoader, this.beanFactory));
 	}
 
+	/**
+	 * Checks if at least one {@link RepositoryFragment} indicates need to access to {@link RepositoryMetadata} by being
+	 * flagged with {@link RepositoryMetadataAccess}.
+	 *
+	 * @param fragments
+	 * @return {@literal true} if access to metadata is required.
+	 */
+	private static boolean shouldExposeMetadata(RepositoryFragments fragments) {
+
+		for (RepositoryFragment> fragment : fragments) {
+			if (fragment.getImplementation().filter(RepositoryMetadataAccess.class::isInstance).isPresent()) {
+				return true;
+			}
+		}
+		return false;
+	}
+
 	/**
 	 * Method interceptor that calls methods on the {@link RepositoryComposition}.
 	 *
diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java
index 96a8e9526d..45807de00e 100644
--- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java
+++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java
@@ -20,6 +20,7 @@
 import java.util.Optional;
 import java.util.stream.Stream;
 
+import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.ObjectUtils;
@@ -41,6 +42,7 @@
  * Fragments are immutable.
  *
  * @author Mark Paluch
+ * @author Christoph Strobl
  * @since 2.0
  * @see RepositoryComposition
  */
@@ -53,7 +55,7 @@ public interface RepositoryFragment {
 	 * @return
 	 */
 	static  RepositoryFragment implemented(T implementation) {
-		return new ImplementedRepositoryFragment(Optional.empty(), implementation);
+		return new ImplementedRepositoryFragment<>((Class) null, implementation);
 	}
 
 	/**
@@ -64,7 +66,7 @@ static  RepositoryFragment implemented(T implementation) {
 	 * @return
 	 */
 	static  RepositoryFragment implemented(Class interfaceClass, T implementation) {
-		return new ImplementedRepositoryFragment<>(Optional.of(interfaceClass), implementation);
+		return new ImplementedRepositoryFragment<>(interfaceClass, implementation);
 	}
 
 	/**
@@ -134,7 +136,7 @@ public Class> getSignatureContributor() {
 
 		@Override
 		public RepositoryFragment withImplementation(T implementation) {
-			return new ImplementedRepositoryFragment<>(Optional.of(interfaceOrImplementation), implementation);
+			return new ImplementedRepositoryFragment<>(interfaceOrImplementation, implementation);
 		}
 
 		@Override
@@ -164,9 +166,20 @@ public int hashCode() {
 
 	class ImplementedRepositoryFragment implements RepositoryFragment {
 
-		private final Optional> interfaceClass;
+		private final @Nullable Class interfaceClass;
 		private final T implementation;
-		private final Optional optionalImplementation;
+
+		/**
+		 * Creates a new {@link ImplementedRepositoryFragment} for the given interface class and implementation.
+		 *
+		 * @param interfaceClass
+		 * @param implementation
+		 * @deprecated since 3.4 - use {@link ImplementedRepositoryFragment(Class, Object)} instead.
+		 */
+		@Deprecated(since = "3.4")
+		public ImplementedRepositoryFragment(Optional> interfaceClass, T implementation) {
+			this(interfaceClass.orElse(null), implementation);
+		}
 
 		/**
 		 * Creates a new {@link ImplementedRepositoryFragment} for the given interface class and implementation.
@@ -174,37 +187,36 @@ class ImplementedRepositoryFragment implements RepositoryFragment {
 		 * @param interfaceClass must not be {@literal null}.
 		 * @param implementation must not be {@literal null}.
 		 */
-		public ImplementedRepositoryFragment(Optional> interfaceClass, T implementation) {
+		public ImplementedRepositoryFragment(@Nullable Class interfaceClass, T implementation) {
 
-			Assert.notNull(interfaceClass, "Interface class must not be null");
 			Assert.notNull(implementation, "Implementation object must not be null");
 
-			interfaceClass.ifPresent(it -> {
+			if(interfaceClass != null) {
 
-				Assert.isTrue(ClassUtils.isAssignableValue(it, implementation),
-						() -> String.format("Fragment implementation %s does not implement %s",
-								ClassUtils.getQualifiedName(implementation.getClass()), ClassUtils.getQualifiedName(it)));
-			});
+				Assert.isTrue(ClassUtils.isAssignableValue(interfaceClass, implementation),
+					() -> String.format("Fragment implementation %s does not implement %s",
+						ClassUtils.getQualifiedName(implementation.getClass()), ClassUtils.getQualifiedName(interfaceClass)));
+			};
 
 			this.interfaceClass = interfaceClass;
 			this.implementation = implementation;
-			this.optionalImplementation = Optional.of(implementation);
 		}
 
-		@SuppressWarnings({ "rawtypes", "unchecked" })
 		public Class> getSignatureContributor() {
-			return interfaceClass.orElseGet(() -> {
 
-				if(implementation instanceof Class type) {
-					return type;
-				}
-				return (Class) implementation.getClass();
-			});
+			if(interfaceClass != null) {
+				return interfaceClass;
+			}
+
+			if(implementation instanceof Class> type) {
+				return type;
+			}
+			return implementation.getClass();
 		}
 
 		@Override
 		public Optional getImplementation() {
-			return optionalImplementation;
+			return Optional.of(implementation);
 		}
 
 		@Override
@@ -216,7 +228,7 @@ public RepositoryFragment withImplementation(T implementation) {
 		public String toString() {
 
 			return String.format("ImplementedRepositoryFragment %s%s",
-					interfaceClass.map(ClassUtils::getShortName).map(it -> it + ":").orElse(""),
+					interfaceClass != null ? (ClassUtils.getShortName(interfaceClass) + ";") : "",
 					ClassUtils.getShortName(implementation.getClass()));
 		}
 
@@ -235,18 +247,13 @@ public boolean equals(Object o) {
 				return false;
 			}
 
-			if (!ObjectUtils.nullSafeEquals(implementation, that.implementation)) {
-				return false;
-			}
-
-			return ObjectUtils.nullSafeEquals(optionalImplementation, that.optionalImplementation);
+			return ObjectUtils.nullSafeEquals(implementation, that.implementation);
 		}
 
 		@Override
 		public int hashCode() {
 			int result = ObjectUtils.nullSafeHashCode(interfaceClass);
 			result = 31 * result + ObjectUtils.nullSafeHashCode(implementation);
-			result = 31 * result + ObjectUtils.nullSafeHashCode(optionalImplementation);
 			return result;
 		}
 	}
diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryMetadataAccess.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryMetadataAccess.java
new file mode 100644
index 0000000000..fb4a7b82ce
--- /dev/null
+++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryMetadataAccess.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2024 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.repository.core.support;
+
+/**
+ * Marker for repository fragment implementation that intend to access repository method invocation metadata.
+ * 
+ * Note that this is a marker interface in the style of {@link java.io.Serializable}, semantically applying to a
+ * fragment implementation class. In other words, this marker applies to a particular repository composition that
+ * enables metadata access for the repository proxy when the composition contain fragments implementing this interface.
+ * 
+ * Ideally, in a repository composition only the fragment implementation uses this interface while the fragment
+ * interface does not.
+ *
+ * @author Mark Paluch
+ * @since 3.4
+ * @see RepositoryMethodContext
+ */
+public interface RepositoryMetadataAccess {
+
+}
diff --git a/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java
index 79f2862bf9..2661998cb2 100755
--- a/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java
+++ b/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java
@@ -271,6 +271,27 @@ record Metadata(RepositoryMethodContext context, MethodInvocation methodInvocati
 		assertThat(metadata.methodInvocation().getMethod().getName()).isEqualTo("findMetadataByLastname");
 	}
 
+	@Test // GH-3090
+	void capturesRepositoryMetadataWithMetadataAccess() {
+
+		record Metadata(RepositoryMethodContext context, MethodInvocation methodInvocation) {
+		}
+
+		when(factory.queryOne.execute(any(Object[].class)))
+				.then(invocation -> new Metadata(RepositoryMethodContext.currentMethod(),
+						ExposeInvocationInterceptor.currentInvocation()));
+
+		var repository = factory.getRepository(ObjectRepository.class, new RepositoryMetadataAccess() {});
+		var metadataByLastname = repository.findMetadataByLastname();
+
+		assertThat(metadataByLastname).isInstanceOf(Metadata.class);
+
+		Metadata metadata = (Metadata) metadataByLastname;
+		assertThat(metadata.context().getMethod().getName()).isEqualTo("findMetadataByLastname");
+		assertThat(metadata.context().getRepository().getDomainType()).isEqualTo(Object.class);
+		assertThat(metadata.methodInvocation().getMethod().getName()).isEqualTo("findMetadataByLastname");
+	}
+
 	@Test // DATACMNS-509, DATACMNS-1764
 	void convertsWithSameElementType() {