diff --git a/.mvn/jvm.config b/.mvn/jvm.config
index 32599cefea..e27f6e8f5e 100644
--- a/.mvn/jvm.config
+++ b/.mvn/jvm.config
@@ -8,3 +8,7 @@
 --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
 --add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
 --add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED
+--add-opens=java.base/java.util=ALL-UNNAMED
+--add-opens=java.base/java.lang.reflect=ALL-UNNAMED
+--add-opens=java.base/java.text=ALL-UNNAMED
+--add-opens=java.desktop/java.awt.font=ALL-UNNAMED
diff --git a/Jenkinsfile b/Jenkinsfile
index 0e83b47e2f..4314427b03 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -9,7 +9,7 @@ pipeline {
 
 	triggers {
 		pollSCM 'H/10 * * * *'
-		upstream(upstreamProjects: "spring-data-commons/main", threshold: hudson.model.Result.SUCCESS)
+		upstream(upstreamProjects: "spring-data-commons/4.0.x", threshold: hudson.model.Result.SUCCESS)
 	}
 
 	options {
@@ -20,29 +20,10 @@ pipeline {
 	stages {
 		stage("Docker images") {
 			parallel {
-				stage('Publish JDK (Java 17) + MongoDB 6.0') {
-					when {
-						anyOf {
-							changeset "ci/openjdk17-mongodb-6.0/**"
-							changeset "ci/pipeline.properties"
-						}
-					}
-					agent { label 'data' }
-					options { timeout(time: 30, unit: 'MINUTES') }
-
-					steps {
-						script {
-							def image = docker.build("springci/spring-data-with-mongodb-6.0:${p['java.main.tag']}", "--build-arg BASE=${p['docker.java.main.image']} --build-arg MONGODB=${p['docker.mongodb.6.0.version']} ci/openjdk17-mongodb-6.0/")
-							docker.withRegistry(p['docker.registry'], p['docker.credentials']) {
-								image.push()
-							}
-						}
-					}
-				}
-				stage('Publish JDK (Java 17) + MongoDB 7.0') {
+				stage('Publish JDK (Java 24) + MongoDB 8.0') {
 					when {
 							anyOf {
-								changeset "ci/openjdk17-mongodb-7.0/**"
+								changeset "ci/openjdk24-mongodb-8.0/**"
 								changeset "ci/pipeline.properties"
 							}
 						}
@@ -51,7 +32,7 @@ pipeline {
 
 					steps {
 						script {
-							def image = docker.build("springci/spring-data-with-mongodb-7.0:${p['java.main.tag']}", "--build-arg BASE=${p['docker.java.main.image']} --build-arg MONGODB=${p['docker.mongodb.7.0.version']} ci/openjdk17-mongodb-7.0/")
+							def image = docker.build("springci/spring-data-with-mongodb-8.0:${p['java.main.tag']}", "--build-arg BASE=${p['docker.java.main.image']} --build-arg MONGODB=${p['docker.mongodb.7.0.version']} ci/openjdk24-mongodb-8.0/")
 							docker.withRegistry(p['docker.registry'], p['docker.credentials']) {
 								image.push()
 							}
@@ -61,7 +42,7 @@ pipeline {
 				stage('Publish JDK (Java.next) + MongoDB 8.0') {
 					when {
 						anyOf {
-							changeset "ci/openjdk17-mongodb-8.0/**"
+							changeset "ci/openjdk24-mongodb-8.0/**"
 							changeset "ci/pipeline.properties"
 						}
 					}
@@ -70,7 +51,7 @@ pipeline {
 
 					steps {
 						script {
-							def image = docker.build("springci/spring-data-with-mongodb-8.0:${p['java.next.tag']}", "--build-arg BASE=${p['docker.java.next.image']} --build-arg MONGODB=${p['docker.mongodb.8.0.version']} ci/openjdk23-mongodb-8.0/")
+							def image = docker.build("springci/spring-data-with-mongodb-8.0:${p['java.next.tag']}", "--build-arg BASE=${p['docker.java.next.image']} --build-arg MONGODB=${p['docker.mongodb.8.0.version']} ci/openjdk24-mongodb-8.0/")
 							docker.withRegistry(p['docker.registry'], p['docker.credentials']) {
 								image.push()
 							}
@@ -99,7 +80,7 @@ pipeline {
 			steps {
 				script {
 					docker.withRegistry(p['docker.proxy.registry'], p['docker.proxy.credentials']) {
-						docker.image("springci/spring-data-with-mongodb-6.0:${p['java.main.tag']}").inside(p['docker.java.inside.docker']) {
+						docker.image("springci/spring-data-with-mongodb-8.0:${p['java.main.tag']}").inside(p['docker.java.inside.docker']) {
 							sh 'ci/start-replica.sh'
 							sh 'MAVEN_OPTS="-Duser.name=' + "${p['jenkins.user.name']}" + ' -Duser.home=/tmp/jenkins-home" ' +
 								"./mvnw -s settings.xml -Ddevelocity.storage.directory=/tmp/jenkins-home/.develocity-root -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-mongodb clean dependency:list test -Dsort -U -B"
@@ -118,27 +99,6 @@ pipeline {
 				}
 			}
 			parallel {
-				stage("test: MongoDB 7.0 (main)") {
-					agent {
-						label 'data'
-					}
-					options { timeout(time: 30, unit: 'MINUTES') }
-					environment {
-						ARTIFACTORY = credentials("${p['artifactory.credentials']}")
-						DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}")
-					}
-					steps {
-						script {
-							docker.withRegistry(p['docker.proxy.registry'], p['docker.proxy.credentials']) {
-								docker.image("springci/spring-data-with-mongodb-7.0:${p['java.main.tag']}").inside(p['docker.java.inside.docker']) {
-									sh 'ci/start-replica.sh'
-									sh 'MAVEN_OPTS="-Duser.name=' + "${p['jenkins.user.name']}" + ' -Duser.home=/tmp/jenkins-home" ' +
-										"./mvnw -s settings.xml -Ddevelocity.storage.directory=/tmp/jenkins-home/.develocity-root -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-mongodb clean dependency:list test -Dsort -U -B"
-								}
-							}
-						}
-					}
-				}
 
 				stage("test: MongoDB 8.0") {
 					agent {
diff --git a/ci/openjdk17-mongodb-6.0/Dockerfile b/ci/openjdk17-mongodb-6.0/Dockerfile
deleted file mode 100644
index fd2580e23a..0000000000
--- a/ci/openjdk17-mongodb-6.0/Dockerfile
+++ /dev/null
@@ -1,25 +0,0 @@
-ARG BASE
-FROM ${BASE}
-# Any ARG statements before FROM are cleared.
-ARG MONGODB
-
-ENV TZ=Etc/UTC
-ENV DEBIAN_FRONTEND=noninteractive
-ENV MONGO_VERSION=${MONGODB}
-
-RUN set -eux; \
-	sed -i -e 's/archive.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list && \
-	sed -i -e 's/security.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list && \
-    sed -i -e 's/ports.ubuntu.com/mirrors.ocf.berkeley.edu/g' /etc/apt/sources.list && \
-	sed -i -e 's/http/https/g' /etc/apt/sources.list && \
-	apt-get update && apt-get install -y apt-transport-https apt-utils gnupg2 wget && \
-	# MongoDB 6.0 release signing key
-	wget -qO - https://www.mongodb.org/static/pgp/server-6.0.asc | apt-key add - && \
-	# Needed when MongoDB creates a 6.0 folder.
-	echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/6.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-6.0.list && \
-	echo ${TZ} > /etc/timezone
-
-RUN apt-get update && \
-	apt-get install -y mongodb-org=${MONGODB} mongodb-org-server=${MONGODB} mongodb-org-shell=${MONGODB} mongodb-org-mongos=${MONGODB} mongodb-org-tools=${MONGODB} && \
-	apt-get clean && \
-	rm -rf /var/lib/apt/lists/*
diff --git a/ci/openjdk17-mongodb-7.0/Dockerfile b/ci/openjdk17-mongodb-7.0/Dockerfile
deleted file mode 100644
index 5701ab9fbc..0000000000
--- a/ci/openjdk17-mongodb-7.0/Dockerfile
+++ /dev/null
@@ -1,25 +0,0 @@
-ARG BASE
-FROM ${BASE}
-# Any ARG statements before FROM are cleared.
-ARG MONGODB
-
-ENV TZ=Etc/UTC
-ENV DEBIAN_FRONTEND=noninteractive
-ENV MONGO_VERSION=${MONGODB}
-
-RUN set -eux; \
-	sed -i -e 's/archive.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list && \
-	sed -i -e 's/security.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list && \
-    sed -i -e 's/ports.ubuntu.com/mirrors.ocf.berkeley.edu/g' /etc/apt/sources.list && \
-	sed -i -e 's/http/https/g' /etc/apt/sources.list && \
-	apt-get update && apt-get install -y apt-transport-https apt-utils gnupg2 wget && \
-	# MongoDB 6.0 release signing key
-	wget -qO - https://www.mongodb.org/static/pgp/server-7.0.asc | apt-key add - && \
-	# Needed when MongoDB creates a 7.0 folder.
-	echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/7.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-7.0.list && \
-	echo ${TZ} > /etc/timezone
-
-RUN apt-get update && \
-	apt-get install -y mongodb-org=${MONGODB} mongodb-org-server=${MONGODB} mongodb-org-shell=${MONGODB} mongodb-org-mongos=${MONGODB} mongodb-org-tools=${MONGODB} && \
-	apt-get clean && \
-	rm -rf /var/lib/apt/lists/*
diff --git a/ci/openjdk23-mongodb-8.0/Dockerfile b/ci/openjdk24-mongodb-8.0/Dockerfile
similarity index 100%
rename from ci/openjdk23-mongodb-8.0/Dockerfile
rename to ci/openjdk24-mongodb-8.0/Dockerfile
diff --git a/ci/pipeline.properties b/ci/pipeline.properties
index 9eb163fde7..4beebb0dfe 100644
--- a/ci/pipeline.properties
+++ b/ci/pipeline.properties
@@ -1,19 +1,18 @@
 # Java versions
-java.main.tag=17.0.13_11-jdk-focal
-java.next.tag=23.0.1_11-jdk-noble
+java.main.tag=24.0.1_9-jdk-noble
+java.next.tag=24.0.1_9-jdk-noble
 
 # Docker container images - standard
 docker.java.main.image=library/eclipse-temurin:${java.main.tag}
 docker.java.next.image=library/eclipse-temurin:${java.next.tag}
 
 # Supported versions of MongoDB
-docker.mongodb.6.0.version=6.0.10
-docker.mongodb.7.0.version=7.0.2
-docker.mongodb.8.0.version=8.0.0
+docker.mongodb.8.0.version=8.0.9
 
 # Supported versions of Redis
 docker.redis.6.version=6.2.13
 docker.redis.7.version=7.2.4
+docker.valkey.8.version=8.1.1
 
 # Docker environment settings
 docker.java.inside.basic=-v $HOME:/tmp/jenkins-home
diff --git a/pom.xml b/pom.xml
index 9f4b6bc897..95fc8379d9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
 
 	<groupId>org.springframework.data</groupId>
 	<artifactId>spring-data-mongodb-parent</artifactId>
-	<version>4.5.0-SNAPSHOT</version>
+	<version>5.0.0-SNAPSHOT</version>
 	<packaging>pom</packaging>
 
 	<name>Spring Data MongoDB</name>
@@ -15,7 +15,7 @@
 	<parent>
 		<groupId>org.springframework.data.build</groupId>
 		<artifactId>spring-data-parent</artifactId>
-		<version>3.5.0-SNAPSHOT</version>
+		<version>4.0.0-SNAPSHOT</version>
 	</parent>
 
 	<modules>
@@ -26,8 +26,8 @@
 	<properties>
 		<project.type>multi</project.type>
 		<dist.id>spring-data-mongodb</dist.id>
-		<springdata.commons>3.5.0-SNAPSHOT</springdata.commons>
-		<mongo>5.4.0</mongo>
+		<springdata.commons>4.0.0-SNAPSHOT</springdata.commons>
+		<mongo>5.5.0</mongo>
 		<jmh.version>1.19</jmh.version>
 	</properties>
 
diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml
index 58c63dfc97..fc88571622 100644
--- a/spring-data-mongodb-distribution/pom.xml
+++ b/spring-data-mongodb-distribution/pom.xml
@@ -15,7 +15,7 @@
 	<parent>
 		<groupId>org.springframework.data</groupId>
 		<artifactId>spring-data-mongodb-parent</artifactId>
-		<version>4.5.0-SNAPSHOT</version>
+		<version>5.0.0-SNAPSHOT</version>
 		<relativePath>../pom.xml</relativePath>
 	</parent>
 
diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml
index 096fd48022..6f34da5660 100644
--- a/spring-data-mongodb/pom.xml
+++ b/spring-data-mongodb/pom.xml
@@ -13,7 +13,7 @@
 	<parent>
 		<groupId>org.springframework.data</groupId>
 		<artifactId>spring-data-mongodb-parent</artifactId>
-		<version>4.5.0-SNAPSHOT</version>
+		<version>5.0.0-SNAPSHOT</version>
 		<relativePath>../pom.xml</relativePath>
 	</parent>
 
@@ -67,12 +67,6 @@
 		<dependency>
 			<groupId>org.springframework</groupId>
 			<artifactId>spring-core</artifactId>
-			<exclusions>
-				<exclusion>
-					<groupId>commons-logging</groupId>
-					<artifactId>commons-logging</artifactId>
-				</exclusion>
-			</exclusions>
 		</dependency>
 		<dependency>
 			<groupId>org.springframework</groupId>
@@ -135,7 +129,7 @@
 		<dependency>
 			<groupId>org.awaitility</groupId>
 			<artifactId>awaitility</artifactId>
-			<version>4.2.2</version>
+			<version>${awaitility}</version>
 			<scope>test</scope>
 		</dependency>
 
@@ -146,6 +140,13 @@
 			<optional>true</optional>
 		</dependency>
 
+		<dependency>
+			<groupId>net.javacrumbs.json-unit</groupId>
+			<artifactId>json-unit-assertj</artifactId>
+			<version>4.1.0</version>
+			<scope>test</scope>
+		</dependency>
+
 		<!-- CDI -->
 		<!-- Dependency order required to build against CDI 1.0 and test with CDI 2.0 -->
 		<dependency>
@@ -294,6 +295,12 @@
 			<scope>test</scope>
 		</dependency>
 
+		<dependency>
+			<groupId>org.springframework</groupId>
+			<artifactId>spring-core-test</artifactId>
+			<scope>test</scope>
+		</dependency>
+
 		<!-- Kotlin extension -->
 		<dependency>
 			<groupId>org.jetbrains.kotlin</groupId>
@@ -359,8 +366,76 @@
 
 	</dependencies>
 
-	<build>
+	<profiles>
+		<profile>
+			<id>nullaway</id>
+			<build>
+				<plugins>
+					<plugin>
+						<groupId>org.apache.maven.plugins</groupId>
+						<artifactId>maven-compiler-plugin</artifactId>
+						<configuration>
+							<annotationProcessorPaths>
+								<path>
+									<groupId>com.querydsl</groupId>
+									<artifactId>querydsl-apt</artifactId>
+									<version>${querydsl}</version>
+								</path>
+								<path>
+									<groupId>org.openjdk.jmh</groupId>
+									<artifactId>jmh-generator-annprocess</artifactId>
+									<version>${jmh}</version>
+								</path>
+								<path>
+									<groupId>com.google.errorprone</groupId>
+									<artifactId>error_prone_core</artifactId>
+									<version>${errorprone}</version>
+								</path>
+								<path>
+									<groupId>com.uber.nullaway</groupId>
+									<artifactId>nullaway</artifactId>
+									<version>${nullaway}</version>
+								</path>
+							</annotationProcessorPaths>
+						</configuration>
+						<executions>
+							<execution>
+								<id>default-compile</id>
+								<phase>none</phase>
+							</execution>
+							<execution>
+								<id>default-testCompile</id>
+								<phase>none</phase>
+							</execution>
+							<execution>
+								<id>java-compile</id>
+								<phase>compile</phase>
+								<goals>
+									<goal>compile</goal>
+								</goals>
+								<configuration>
+									<compilerArgs>
+										<arg>-XDcompilePolicy=simple</arg>
+										<arg>--should-stop=ifError=FLOW</arg>
+										<arg>-Xplugin:ErrorProne -XepDisableAllChecks -Xep:NullAway:ERROR -XepOpt:NullAway:OnlyNullMarked=true -XepOpt:NullAway:TreatGeneratedAsUnannotated=true -XepOpt:NullAway:CustomContractAnnotations=org.springframework.lang.Contract</arg>
+									</compilerArgs>
+								</configuration>
+							</execution>
+							<execution>
+								<id>java-test-compile</id>
+								<phase>test-compile</phase>
+								<goals>
+									<goal>testCompile</goal>
+								</goals>
+							</execution>
+						</executions>
+					</plugin>
+				</plugins>
+			</build>
+		</profile>
+	</profiles>
 
+	<build>
 		<plugins>
 
 			<plugin>
diff --git a/spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/AotRepositoryBenchmark.java b/spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/AotRepositoryBenchmark.java
new file mode 100644
index 0000000000..ba9da66da4
--- /dev/null
+++ b/spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/AotRepositoryBenchmark.java
@@ -0,0 +1,153 @@
+/*
+ * 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.mongodb.repository;
+
+import org.junit.platform.commons.annotation.Testable;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+
+import org.springframework.aot.test.generate.TestGenerationContext;
+import org.springframework.core.test.tools.TestCompiler;
+import org.springframework.data.mongodb.core.MongoOperations;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.mongodb.microbenchmark.AbstractMicrobenchmark;
+import org.springframework.data.mongodb.repository.aot.MongoRepositoryContributor;
+import org.springframework.data.mongodb.repository.aot.TestMongoAotRepositoryContext;
+import org.springframework.data.mongodb.repository.support.MongoRepositoryFactory;
+import org.springframework.data.mongodb.repository.support.QuerydslMongoPredicateExecutor;
+import org.springframework.data.mongodb.repository.support.SimpleMongoRepository;
+import org.springframework.data.projection.ProjectionFactory;
+import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.core.support.RepositoryComposition;
+import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
+import org.springframework.data.repository.core.support.RepositoryFragment;
+import org.springframework.data.repository.query.ValueExpressionDelegate;
+
+import com.mongodb.client.MongoClient;
+import com.mongodb.client.MongoClients;
+
+/**
+ * Benchmark for AOT repositories.
+ *
+ * @author Mark Paluch
+ */
+@Testable
+public class AotRepositoryBenchmark extends AbstractMicrobenchmark {
+
+	@State(Scope.Benchmark)
+	public static class BenchmarkParameters {
+
+		public static Class<?> aot;
+		public static TestMongoAotRepositoryContext repositoryContext = new TestMongoAotRepositoryContext(
+				SmallerPersonRepository.class,
+				RepositoryComposition.of(RepositoryFragment.structural(SimpleMongoRepository.class),
+						RepositoryFragment.structural(QuerydslMongoPredicateExecutor.class)));
+
+		MongoClient mongoClient;
+		MongoTemplate mongoTemplate;
+		RepositoryComposition.RepositoryFragments fragments;
+		SmallerPersonRepository repositoryProxy;
+
+		@Setup(Level.Trial)
+		public void doSetup() {
+
+			mongoClient = MongoClients.create();
+			mongoTemplate = new MongoTemplate(mongoClient, "jmh");
+
+			if (this.aot == null) {
+
+				TestGenerationContext generationContext = new TestGenerationContext(PersonRepository.class);
+
+				new MongoRepositoryContributor(repositoryContext).contribute(generationContext);
+
+				TestCompiler.forSystem().withCompilerOptions("-parameters").with(generationContext).compile(compiled -> {
+
+					try {
+						this.aot = compiled.getClassLoader().loadClass(SmallerPersonRepository.class.getName() + "Impl__Aot");
+					} catch (Exception e) {
+						throw new RuntimeException(e);
+					}
+				});
+			}
+
+			try {
+				RepositoryFactoryBeanSupport.FragmentCreationContext creationContext = getCreationContext(repositoryContext);
+				fragments = RepositoryComposition.RepositoryFragments
+						.just(aot.getConstructor(MongoOperations.class, RepositoryFactoryBeanSupport.FragmentCreationContext.class)
+								.newInstance(mongoTemplate, creationContext));
+
+				this.repositoryProxy = createRepository(fragments);
+			} catch (Exception e) {
+				throw new RuntimeException(e);
+			}
+		}
+
+		private RepositoryFactoryBeanSupport.FragmentCreationContext getCreationContext(
+				TestMongoAotRepositoryContext repositoryContext) {
+
+			RepositoryFactoryBeanSupport.FragmentCreationContext creationContext = new RepositoryFactoryBeanSupport.FragmentCreationContext() {
+				@Override
+				public RepositoryMetadata getRepositoryMetadata() {
+					return repositoryContext.getRepositoryInformation();
+				}
+
+				@Override
+				public ValueExpressionDelegate getValueExpressionDelegate() {
+					return ValueExpressionDelegate.create();
+				}
+
+				@Override
+				public ProjectionFactory getProjectionFactory() {
+					return new SpelAwareProxyProjectionFactory();
+				}
+			};
+
+			return creationContext;
+		}
+
+		@TearDown(Level.Trial)
+		public void doTearDown() {
+			mongoClient.close();
+		}
+
+		public SmallerPersonRepository createRepository(RepositoryComposition.RepositoryFragments fragments) {
+			MongoRepositoryFactory repositoryFactory = new MongoRepositoryFactory(mongoTemplate);
+			return repositoryFactory.getRepository(SmallerPersonRepository.class, fragments);
+		}
+
+	}
+
+	@Benchmark
+	public SmallerPersonRepository repositoryBootstrap(BenchmarkParameters parameters) {
+		return parameters.createRepository(parameters.fragments);
+	}
+
+	@Benchmark
+	public Object findDerived(BenchmarkParameters parameters) {
+		return parameters.repositoryProxy.findByFirstname("foo");
+	}
+
+	@Benchmark
+	public Object findAnnotated(BenchmarkParameters parameters) {
+		return parameters.repositoryProxy.findByThePersonsFirstname("foo");
+	}
+
+}
diff --git a/spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/SmallerPersonRepository.java b/spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/SmallerPersonRepository.java
new file mode 100644
index 0000000000..bc3868e052
--- /dev/null
+++ b/spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/SmallerPersonRepository.java
@@ -0,0 +1,477 @@
+/*
+ * Copyright 2010-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.mongodb.repository;
+
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.stream.Stream;
+
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.data.domain.Limit;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.ScrollPosition;
+import org.springframework.data.domain.Slice;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Window;
+import org.springframework.data.mongodb.core.aggregation.AggregationResults;
+import org.springframework.data.mongodb.core.query.UpdateDefinition;
+import org.springframework.data.mongodb.repository.Person.Sex;
+import org.springframework.data.querydsl.QuerydslPredicateExecutor;
+import org.springframework.data.repository.query.Param;
+
+/**
+ * Sample repository managing {@link Person} entities.
+ *
+ * @author Oliver Gierke
+ * @author Thomas Darimont
+ * @author Christoph Strobl
+ * @author Fırat KÜÇÜK
+ * @author Mark Paluch
+ */
+public interface SmallerPersonRepository extends MongoRepository<Person, String>, QuerydslPredicateExecutor<Person> {
+
+	/**
+	 * Returns all {@link Person}s with the given lastname.
+	 *
+	 * @param lastname
+	 * @return
+	 */
+	List<Person> findByLastname(String lastname);
+
+	List<Person> findByLastnameStartsWith(String prefix);
+
+	List<Person> findByLastnameEndsWith(String postfix);
+
+	/**
+	 * Returns all {@link Person}s with the given lastname ordered by their firstname.
+	 *
+	 * @param lastname
+	 * @return
+	 */
+	List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
+
+	/**
+	 * Returns the {@link Person}s with the given firstname. Uses {@link Query} annotation to define the query to be
+	 * executed.
+	 *
+	 * @param firstname
+	 * @return
+	 */
+	@Query(value = "{ 'lastname' : ?0 }", fields = "{ 'firstname': 1, 'lastname': 1}")
+	List<Person> findByThePersonsLastname(String lastname);
+
+	/**
+	 * Returns the {@link Person}s with the given firstname. Uses {@link Query} annotation to define the query to be
+	 * executed.
+	 *
+	 * @param firstname
+	 * @return
+	 */
+	@Query(value = "{ 'firstname' : ?0 }", fields = "{ 'firstname': 1, 'lastname': 1}")
+	List<Person> findByThePersonsFirstname(String firstname);
+
+	// DATAMONGO-871
+	@Query(value = "{ 'firstname' : ?0 }")
+	Person[] findByThePersonsFirstnameAsArray(String firstname);
+
+	/**
+	 * Returns all {@link Person}s with a firstname matching the given one (*-wildcard supported).
+	 *
+	 * @param firstname
+	 * @return
+	 */
+	List<Person> findByFirstnameLike(@Nullable String firstname);
+
+	List<Person> findByFirstnameNotContains(String firstname);
+
+	/**
+	 * Returns all {@link Person}s with a firstname not matching the given one (*-wildcard supported).
+	 *
+	 * @param firstname
+	 * @return
+	 */
+	List<Person> findByFirstnameNotLike(String firstname);
+
+	List<Person> findByFirstnameLikeOrderByLastnameAsc(String firstname, Sort sort);
+
+	List<Person> findBySkillsContains(List<String> skills);
+
+	List<Person> findBySkillsNotContains(List<String> skills);
+
+	@Query("{'age' : { '$lt' : ?0 } }")
+	List<Person> findByAgeLessThan(int age, Sort sort);
+
+	/**
+	 * Returns a scroll of {@link Person}s with a lastname matching the given one (*-wildcards supported).
+	 *
+	 * @param lastname
+	 * @param scrollPosition
+	 * @return
+	 */
+	Window<Person> findTop2ByLastnameLikeOrderByLastnameAscFirstnameAsc(String lastname, ScrollPosition scrollPosition);
+
+	Window<Person> findByLastnameLikeOrderByLastnameAscFirstnameAsc(String lastname, ScrollPosition scrollPosition,
+			Limit limit);
+
+	/**
+	 * Returns a scroll of {@link Person}s applying projections with a lastname matching the given one (*-wildcards
+	 * supported).
+	 *
+	 * @param lastname
+	 * @param pageable
+	 * @return
+	 */
+	Window<PersonSummaryDto> findCursorProjectionByLastnameLike(String lastname, Pageable pageable);
+
+	/**
+	 * Returns a page of {@link Person}s with a lastname matching the given one (*-wildcards supported).
+	 *
+	 * @param lastname
+	 * @param pageable
+	 * @return
+	 */
+	Page<Person> findByLastnameLike(String lastname, Pageable pageable);
+
+	List<Person> findByLastnameLike(String lastname, Sort sort, Limit limit);
+
+	@Query("{ 'lastname' : { '$regex' : '?0', '$options' : 'i'}}")
+	Page<Person> findByLastnameLikeWithPageable(String lastname, Pageable pageable);
+
+	List<Person> findByFirstname(String firstname);
+
+	List<Person> findByLastnameIgnoreCaseIn(String... lastname);
+
+	/**
+	 * Returns all {@link Person}s with a firstname contained in the given varargs.
+	 *
+	 * @param firstnames
+	 * @return
+	 */
+	List<Person> findByFirstnameIn(String... firstnames);
+
+	/**
+	 * Returns all {@link Person}s with a firstname not contained in the given collection.
+	 *
+	 * @param firstnames
+	 * @return
+	 */
+	List<Person> findByFirstnameNotIn(Collection<String> firstnames);
+
+	List<Person> findByFirstnameAndLastname(String firstname, String lastname);
+
+	/**
+	 * Returns all {@link Person}s with an age between the two given values.
+	 *
+	 * @param from
+	 * @param to
+	 * @return
+	 */
+	List<Person> findByAgeBetween(int from, int to);
+
+	/**
+	 * Returns the {@link Person} with the given {@link Address} as shipping address.
+	 *
+	 * @param address
+	 * @return
+	 */
+	Person findByShippingAddresses(Address address);
+
+	/**
+	 * Returns all {@link Person}s with the given {@link Address}.
+	 *
+	 * @param address
+	 * @return
+	 */
+	List<Person> findByAddress(Address address);
+
+	List<Person> findByAddressZipCode(String zipCode);
+
+	List<Person> findByLastnameLikeAndAgeBetween(String lastname, int from, int to);
+
+	List<Person> findByAgeOrLastnameLikeAndFirstnameLike(int age, String lastname, String firstname);
+
+	// TODO: List<Person> findByLocationNear(Point point);
+
+	// TODO: List<Person> findByLocationWithin(Circle circle);
+
+	// TODO: List<Person> findByLocationWithin(Box box);
+
+	// TODO: List<Person> findByLocationWithin(Polygon polygon);
+
+	List<Person> findBySex(Sex sex);
+
+	List<Person> findBySex(Sex sex, Pageable pageable);
+
+	// TODO: List<Person> findByNamedQuery(String firstname);
+
+	List<Person> findByCreator(User user);
+
+	// DATAMONGO-425
+	List<Person> findByCreatedAtLessThan(Date date);
+
+	// DATAMONGO-425
+	List<Person> findByCreatedAtGreaterThan(Date date);
+
+	// DATAMONGO-425
+	@Query("{ 'createdAt' : { '$lt' : ?0 }}")
+	List<Person> findByCreatedAtLessThanManually(Date date);
+
+	// DATAMONGO-427
+	List<Person> findByCreatedAtBefore(Date date);
+
+	// DATAMONGO-427
+	List<Person> findByCreatedAtAfter(Date date);
+
+	// DATAMONGO-472
+	List<Person> findByLastnameNot(String lastname);
+
+	// DATAMONGO-600
+	List<Person> findByCredentials(Credentials credentials);
+
+	// DATAMONGO-636
+	long countByLastname(String lastname);
+
+	// DATAMONGO-636
+	int countByFirstname(String firstname);
+
+	// DATAMONGO-636
+	@Query(value = "{ 'lastname' : ?0 }", count = true)
+	long someCountQuery(String lastname);
+
+	// DATAMONGO-1454
+	boolean existsByFirstname(String firstname);
+
+	// DATAMONGO-1454
+	@ExistsQuery(value = "{ 'lastname' : ?0 }")
+	boolean someExistQuery(String lastname);
+
+	// DATAMONGO-770
+	List<Person> findByFirstnameIgnoreCase(@Nullable String firstName);
+
+	// DATAMONGO-770
+	List<Person> findByFirstnameNotIgnoreCase(String firstName);
+
+	// DATAMONGO-770
+	List<Person> findByFirstnameStartingWithIgnoreCase(String firstName);
+
+	// DATAMONGO-770
+	List<Person> findByFirstnameEndingWithIgnoreCase(String firstName);
+
+	// DATAMONGO-770
+	List<Person> findByFirstnameContainingIgnoreCase(String firstName);
+
+	// DATAMONGO-870
+	Slice<Person> findByAgeGreaterThan(int age, Pageable pageable);
+
+	// DATAMONGO-821
+	@Query("{ creator : { $exists : true } }")
+	Page<Person> findByHavingCreator(Pageable page);
+
+	// DATAMONGO-566
+	List<Person> deleteByLastname(String lastname);
+
+	// DATAMONGO-566
+	Long deletePersonByLastname(String lastname);
+
+	// DATAMONGO-1997
+	Optional<Person> deleteOptionalByLastname(String lastname);
+
+	// DATAMONGO-566
+	@Query(value = "{ 'lastname' : ?0 }", delete = true)
+	List<Person> removeByLastnameUsingAnnotatedQuery(String lastname);
+
+	// DATAMONGO-566
+	@Query(value = "{ 'lastname' : ?0 }", delete = true)
+	Long removePersonByLastnameUsingAnnotatedQuery(String lastname);
+
+	// DATAMONGO-893
+	Page<Person> findByAddressIn(List<Address> address, Pageable page);
+
+	// DATAMONGO-745
+	@Query("{firstname:{$in:?0}, lastname:?1}")
+	Page<Person> findByCustomQueryFirstnamesAndLastname(List<String> firstnames, String lastname, Pageable page);
+
+	// DATAMONGO-745
+	@Query("{lastname:?0, 'address.street':{$in:?1}}")
+	Page<Person> findByCustomQueryLastnameAndAddressStreetInList(String lastname, List<String> streetNames,
+			Pageable page);
+
+	// DATAMONGO-950
+	List<Person> findTop3ByLastnameStartingWith(String lastname);
+
+	// DATAMONGO-950
+	Page<Person> findTop3ByLastnameStartingWith(String lastname, Pageable pageRequest);
+
+	// DATAMONGO-1865
+	Person findFirstBy(); // limits to 1 result if more, just return the first one
+
+	// DATAMONGO-1865
+	Person findPersonByLastnameLike(String firstname); // single person, error if more than one
+
+	// DATAMONGO-1865
+	Optional<Person> findOptionalPersonByLastnameLike(String firstname); // optional still, error when more than one
+
+	// DATAMONGO-1030
+	PersonSummaryDto findSummaryByLastname(String lastname);
+
+	PersonSummaryWithOptional findSummaryWithOptionalByLastname(String lastname);
+
+	@Query("{ ?0 : ?1 }")
+	List<Person> findByKeyValue(String key, String value);
+
+	// DATAMONGO-1165
+	@Query("{ firstname : { $in : ?0 }}")
+	Stream<Person> findByCustomQueryWithStreamingCursorByFirstnames(List<String> firstnames);
+
+	// DATAMONGO-990
+	@Query("{ firstname : ?#{[0]}}")
+	List<Person> findWithSpelByFirstnameForSpELExpressionWithParameterIndexOnly(String firstname);
+
+	// DATAMONGO-990
+	@Query("{ firstname : ?#{[0]}, email: ?#{principal.email} }")
+	List<Person> findWithSpelByFirstnameAndCurrentUserWithCustomQuery(String firstname);
+
+	// DATAMONGO-990
+	@Query("{ firstname : :#{#firstname}}")
+	List<Person> findWithSpelByFirstnameForSpELExpressionWithParameterVariableOnly(@Param("firstname") String firstname);
+
+	// DATAMONGO-1911
+	@Query("{ uniqueId: ?0}")
+	Person findByUniqueId(UUID uniqueId);
+
+	/**
+	 * Returns the count of {@link Person} with the given firstname. Uses {@link CountQuery} annotation to define the
+	 * query to be executed.
+	 *
+	 * @param firstname
+	 * @return
+	 */
+	@CountQuery("{ 'firstname' : ?0 }") // DATAMONGO-1539
+	long countByThePersonsFirstname(String firstname);
+
+	/**
+	 * Deletes {@link Person} entities with the given firstname. Uses {@link DeleteQuery} annotation to define the query
+	 * to be executed.
+	 *
+	 * @param firstname
+	 */
+	@DeleteQuery("{ 'firstname' : ?0 }") // DATAMONGO-1539
+	void deleteByThePersonsFirstname(String firstname);
+
+	// DATAMONGO-1752
+	Iterable<PersonExcerpt> findOpenProjectionBy();
+
+	// DATAMONGO-1752
+	Iterable<PersonSummary> findClosedProjectionBy();
+
+	@Query(sort = "{ age : -1 }")
+	List<Person> findByAgeGreaterThan(int age);
+
+	@Query(sort = "{ age : -1 }")
+	List<Person> findByAgeGreaterThan(int age, Sort sort);
+
+	// TODO: List<Person> findByFirstnameRegex(Pattern pattern);
+
+	@Query(value = "{ 'id' : ?0 }", fields = "{ 'fans': { '$slice': [ ?1, ?2 ] } }")
+	Person findWithSliceInProjection(String id, int skip, int limit);
+
+	@Query(value = "{ 'id' : ?0 }", fields = "{ 'firstname': { '$toUpper': '$firstname' } }")
+	Person findWithAggregationInProjection(String id);
+
+	@Query(value = "{ 'shippingAddresses' : { '$elemMatch' : { 'city' : { '$eq' : 'lnz' } } } }",
+			fields = "{ 'shippingAddresses.$': ?0 }")
+	Person findWithArrayPositionInProjection(int position);
+
+	@Query(value = "{ 'fans' : { '$elemMatch' : { '$ref' : 'user' } } }", fields = "{ 'fans.$': ?0 }")
+	Person findWithArrayPositionInProjectionWithDbRef(int position);
+
+	@Aggregation("{ '$project': { '_id' : '$lastname' } }")
+	List<String> findAllLastnames();
+
+	@Aggregation("{ '$project': { '_id' : '$lastname' } }")
+	Stream<String> findAllLastnamesAsStream();
+
+	@Aggregation("{ '$group': { '_id' : '$lastname', names : { $addToSet : '$?0' } } }")
+	Stream<PersonAggregate> groupStreamByLastnameAnd(String property);
+
+	@Aggregation("{ '$group': { '_id' : '$lastname', names : { $addToSet : '$?0' } } }")
+	List<PersonAggregate> groupByLastnameAnd(String property);
+
+	@Aggregation("{ '$group': { '_id' : '$lastname', names : { $addToSet : '$?0' } } }")
+	Slice<PersonAggregate> groupByLastnameAndAsSlice(String property, Pageable pageable);
+
+	@Aggregation("{ '$group': { '_id' : '$lastname', names : { $addToSet : '$?0' } } }")
+	List<PersonAggregate> groupByLastnameAnd(String property, Sort sort);
+
+	@Aggregation("{ '$group': { '_id' : '$lastname', names : { $addToSet : '$?0' } } }")
+	List<PersonAggregate> groupByLastnameAnd(String property, Pageable page);
+
+	@Aggregation(pipeline = "{ '$group' : { '_id' : null, 'total' : { $sum: '$age' } } }")
+	int sumAge();
+
+	@Aggregation(pipeline = "{ '$group' : { '_id' : null, 'total' : { $sum: '$age' } } }")
+	AggregationResults<org.bson.Document> sumAgeAndReturnAggregationResultWrapper();
+
+	@Aggregation(pipeline = "{ '$group' : { '_id' : null, 'total' : { $sum: '$age' } } }")
+	AggregationResults<SumAge> sumAgeAndReturnAggregationResultWrapperWithConcreteType();
+
+	@Aggregation({ "{ '$match' :  { 'lastname' :  'Matthews'} }",
+			"{ '$project': { _id : 0, firstname : 1, lastname : 1 } }" })
+	Iterable<PersonSummary> findAggregatedClosedInterfaceProjectionBy();
+
+	@Query(value = "{_id:?0}")
+	Optional<org.bson.Document> findDocumentById(String id);
+
+	@Query(value = "{ 'firstname' : ?0, 'lastname' : ?1, 'email' : ?2 , 'age' : ?3, 'sex' : ?4, "
+			+ "'createdAt' : ?5, 'skills' : ?6, 'address.street' : ?7, 'address.zipCode' : ?8, " //
+			+ "'address.city' : ?9, 'uniqueId' : ?10, 'credentials.username' : ?11, 'credentials.password' : ?12 }")
+	Person findPersonByManyArguments(String firstname, String lastname, String email, Integer age, Sex sex,
+			Date createdAt, List<String> skills, String street, String zipCode, //
+			String city, UUID uniqueId, String username, String password);
+
+	List<Person> findByUnwrappedUserUsername(String username);
+
+	List<Person> findByUnwrappedUser(User user);
+
+	int findAndUpdateViaMethodArgAllByLastname(String lastname, UpdateDefinition update);
+
+	@Update("{ '$inc' : { 'visits' : ?1 } }")
+	int findAndIncrementVisitsByLastname(String lastname, int increment);
+
+	@Query("{ 'lastname' : ?0 }")
+	@Update("{ '$inc' : { 'visits' : ?1 } }")
+	int updateAllByLastname(String lastname, int increment);
+
+	@Update(pipeline = { "{ '$set' : { 'visits' : { '$add' : [ '$visits', ?1 ] } } }" })
+	void findAndIncrementVisitsViaPipelineByLastname(String lastname, int increment);
+
+	@Update("{ '$inc' : { 'visits' : ?#{[1]} } }")
+	int findAndIncrementVisitsUsingSpELByLastname(String lastname, int increment);
+
+	@Update("{ '$push' : { 'shippingAddresses' : ?1 } }")
+	int findAndPushShippingAddressByEmail(String email, Address address);
+
+	@Query("{ 'age' : null }")
+	Person findByQueryWithNullEqualityCheck();
+
+	List<Person> findBySpiritAnimal(User user);
+
+}
diff --git a/spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/SmallerRepositoryBenchmark.java b/spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/SmallerRepositoryBenchmark.java
new file mode 100644
index 0000000000..f461a22d31
--- /dev/null
+++ b/spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/SmallerRepositoryBenchmark.java
@@ -0,0 +1,83 @@
+/*
+ * 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.mongodb.repository;
+
+import org.junit.platform.commons.annotation.Testable;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.mongodb.microbenchmark.AbstractMicrobenchmark;
+import org.springframework.data.mongodb.repository.support.MongoRepositoryFactory;
+
+import com.mongodb.client.MongoClient;
+import com.mongodb.client.MongoClients;
+
+/**
+ * Benchmark for AOT repositories.
+ *
+ * @author Mark Paluch
+ */
+@Testable
+public class SmallerRepositoryBenchmark extends AbstractMicrobenchmark {
+
+	@State(Scope.Benchmark)
+	public static class BenchmarkParameters {
+
+		MongoClient mongoClient;
+		MongoTemplate mongoTemplate;
+		SmallerPersonRepository repositoryProxy;
+
+		@Setup(Level.Trial)
+		public void doSetup() {
+
+			mongoClient = MongoClients.create();
+			mongoTemplate = new MongoTemplate(mongoClient, "jmh");
+			repositoryProxy = createRepository();
+		}
+
+		@TearDown(Level.Trial)
+		public void doTearDown() {
+			mongoClient.close();
+		}
+
+		public SmallerPersonRepository createRepository() {
+			MongoRepositoryFactory repositoryFactory = new MongoRepositoryFactory(mongoTemplate);
+			return repositoryFactory.getRepository(SmallerPersonRepository.class);
+		}
+
+	}
+
+	@Benchmark
+	public SmallerPersonRepository repositoryBootstrap(BenchmarkParameters parameters) {
+		return parameters.createRepository();
+	}
+
+	@Benchmark
+	public Object findDerived(BenchmarkParameters parameters) {
+		return parameters.repositoryProxy.findByFirstname("foo");
+	}
+
+	@Benchmark
+	public Object findAnnotated(BenchmarkParameters parameters) {
+		return parameters.repositoryProxy.findByThePersonsFirstname("foo");
+	}
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/BindableMongoExpression.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/BindableMongoExpression.java
index 1f6875c080..3ae41aad35 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/BindableMongoExpression.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/BindableMongoExpression.java
@@ -20,9 +20,10 @@
 import org.bson.Document;
 import org.bson.codecs.DocumentCodec;
 import org.bson.codecs.configuration.CodecRegistry;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec;
 import org.springframework.data.util.Lazy;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
@@ -31,8 +32,7 @@
  * A {@link MongoExpression} using the {@link ParameterBindingDocumentCodec} for parsing a raw ({@literal json})
  * expression. The expression will be wrapped within <code>{ ... }</code> if necessary. The actual parsing and parameter
  * binding of placeholders like {@code ?0} is delayed upon first call on the target {@link Document} via
- * {@link #toDocument()}.
- * <br />
+ * {@link #toDocument()}. <br />
  *
  * <pre class="code">
  * $toUpper : $name                -> { '$toUpper' : '$name' }
@@ -55,7 +55,7 @@ public class BindableMongoExpression implements MongoExpression {
 
 	private final @Nullable CodecRegistryProvider codecRegistryProvider;
 
-	private final @Nullable Object[] args;
+	private final Object @Nullable [] args;
 
 	private final Lazy<Document> target;
 
@@ -63,9 +63,9 @@ public class BindableMongoExpression implements MongoExpression {
 	 * Create a new instance of {@link BindableMongoExpression}.
 	 *
 	 * @param expression must not be {@literal null}.
-	 * @param args can be {@literal null}.
+	 * @param args must not be {@literal null} but may contain {@literal null} elements.
 	 */
-	public BindableMongoExpression(String expression, @Nullable Object[] args) {
+	public BindableMongoExpression(String expression, Object @Nullable [] args) {
 		this(expression, null, args);
 	}
 
@@ -74,10 +74,10 @@ public BindableMongoExpression(String expression, @Nullable Object[] args) {
 	 *
 	 * @param expression must not be {@literal null}.
 	 * @param codecRegistryProvider can be {@literal null}.
-	 * @param args can be {@literal null}.
+	 * @param args must not be {@literal null} but may contain {@literal null} elements.
 	 */
 	public BindableMongoExpression(String expression, @Nullable CodecRegistryProvider codecRegistryProvider,
-			@Nullable Object[] args) {
+			Object @Nullable [] args) {
 
 		Assert.notNull(expression, "Expression must not be null");
 
@@ -93,6 +93,7 @@ public BindableMongoExpression(String expression, @Nullable CodecRegistryProvide
 	 * @param codecRegistry must not be {@literal null}.
 	 * @return new instance of {@link BindableMongoExpression}.
 	 */
+	@Contract("_ -> new")
 	public BindableMongoExpression withCodecRegistry(CodecRegistry codecRegistry) {
 		return new BindableMongoExpression(expressionString, () -> codecRegistry, args);
 	}
@@ -103,6 +104,7 @@ public BindableMongoExpression withCodecRegistry(CodecRegistry codecRegistry) {
 	 * @param args must not be {@literal null}.
 	 * @return new instance of {@link BindableMongoExpression}.
 	 */
+	@Contract("_ -> new")
 	public BindableMongoExpression bind(Object... args) {
 		return new BindableMongoExpression(expressionString, codecRegistryProvider, args);
 	}
@@ -139,7 +141,7 @@ private Document parse() {
 
 	private static String wrapJsonIfNecessary(String json) {
 
-		if(!StringUtils.hasText(json)) {
+		if (!StringUtils.hasText(json)) {
 			return json;
 		}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/BulkOperationException.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/BulkOperationException.java
index b36382a58e..12d8c966af 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/BulkOperationException.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/BulkOperationException.java
@@ -17,6 +17,7 @@
 
 import java.util.List;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.dao.DataAccessException;
 
 import com.mongodb.MongoBulkWriteException;
@@ -40,10 +41,10 @@ public class BulkOperationException extends DataAccessException {
 	/**
 	 * Creates a new {@link BulkOperationException} with the given message and source {@link MongoBulkWriteException}.
 	 *
-	 * @param message must not be {@literal null}.
+	 * @param message can be {@literal null}.
 	 * @param source must not be {@literal null}.
 	 */
-	public BulkOperationException(String message, MongoBulkWriteException source) {
+	public BulkOperationException(@Nullable String message, MongoBulkWriteException source) {
 
 		super(message, source);
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ClientSessionException.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ClientSessionException.java
index 53acf65470..c59eecb43a 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ClientSessionException.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ClientSessionException.java
@@ -15,8 +15,8 @@
  */
 package org.springframework.data.mongodb;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.dao.NonTransientDataAccessException;
-import org.springframework.lang.Nullable;
 
 /**
  * {@link NonTransientDataAccessException} specific to MongoDB {@link com.mongodb.session.ClientSession} related data
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/DefaultMongoTransactionOptionsResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/DefaultMongoTransactionOptionsResolver.java
index c07e2dbe4a..87201ef9ee 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/DefaultMongoTransactionOptionsResolver.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/DefaultMongoTransactionOptionsResolver.java
@@ -18,7 +18,7 @@
 import java.util.Map;
 import java.util.Set;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /**
  * Default implementation of {@link MongoTransactionOptions} using {@literal mongo:} as {@link #getLabelPrefix() label
@@ -42,9 +42,8 @@ public MongoTransactionOptions convert(Map<String, String> options) {
 		return SimpleMongoTransactionOptions.of(options);
 	}
 
-	@Nullable
 	@Override
-	public String getLabelPrefix() {
+	public @Nullable String getLabelPrefix() {
 		return PREFIX;
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDatabaseUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDatabaseUtils.java
index f73f9fb7ed..042a5ba1d3 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDatabaseUtils.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDatabaseUtils.java
@@ -15,7 +15,7 @@
  */
 package org.springframework.data.mongodb;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.transaction.support.ResourceHolderSynchronization;
 import org.springframework.transaction.support.TransactionSynchronization;
 import org.springframework.transaction.support.TransactionSynchronizationManager;
@@ -29,8 +29,7 @@
 /**
  * Helper class for managing a {@link MongoDatabase} instances via {@link MongoDatabaseFactory}. Used for obtaining
  * {@link ClientSession session bound} resources, such as {@link MongoDatabase} and
- * {@link com.mongodb.client.MongoCollection} suitable for transactional usage.
- * <br />
+ * {@link com.mongodb.client.MongoCollection} suitable for transactional usage. <br />
  * <strong>Note:</strong> Intended for internal usage only.
  *
  * @author Christoph Strobl
@@ -42,8 +41,7 @@ public class MongoDatabaseUtils {
 
 	/**
 	 * Obtain the default {@link MongoDatabase database} form the given {@link MongoDatabaseFactory factory} using
-	 * {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}.
-	 * <br />
+	 * {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}. <br />
 	 * Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the current
 	 * {@link Thread} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}.
 	 *
@@ -55,8 +53,7 @@ public static MongoDatabase getDatabase(MongoDatabaseFactory factory) {
 	}
 
 	/**
-	 * Obtain the default {@link MongoDatabase database} form the given {@link MongoDatabaseFactory factory}.
-	 * <br />
+	 * Obtain the default {@link MongoDatabase database} form the given {@link MongoDatabaseFactory factory}. <br />
 	 * Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the current
 	 * {@link Thread} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}.
 	 *
@@ -70,8 +67,7 @@ public static MongoDatabase getDatabase(MongoDatabaseFactory factory, SessionSyn
 
 	/**
 	 * Obtain the {@link MongoDatabase database} with given name form the given {@link MongoDatabaseFactory factory} using
-	 * {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}.
-	 * <br />
+	 * {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}. <br />
 	 * Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the current
 	 * {@link Thread} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}.
 	 *
@@ -139,8 +135,7 @@ public static boolean isTransactionActive(MongoDatabaseFactory dbFactory) {
 		return resourceHolder != null && resourceHolder.hasActiveTransaction();
 	}
 
-	@Nullable
-	private static ClientSession doGetSession(MongoDatabaseFactory dbFactory,
+	private static @Nullable ClientSession doGetSession(MongoDatabaseFactory dbFactory,
 			SessionSynchronization sessionSynchronization) {
 
 		MongoResourceHolder resourceHolder = (MongoResourceHolder) TransactionSynchronizationManager.getResource(dbFactory);
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoResourceHolder.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoResourceHolder.java
index a1e8344a9f..81c25d0998 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoResourceHolder.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoResourceHolder.java
@@ -15,7 +15,7 @@
  */
 package org.springframework.data.mongodb;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.transaction.TransactionDefinition;
 import org.springframework.transaction.support.ResourceHolderSupport;
 
@@ -23,8 +23,7 @@
 
 /**
  * MongoDB specific {@link ResourceHolderSupport resource holder}, wrapping a {@link ClientSession}.
- * {@link MongoTransactionManager} binds instances of this class to the thread.
- * <br />
+ * {@link MongoTransactionManager} binds instances of this class to the thread. <br />
  * <strong>Note:</strong> Intended for internal usage only.
  *
  * @author Christoph Strobl
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionException.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionException.java
index 4215479f62..3d7bec6780 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionException.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionException.java
@@ -15,7 +15,7 @@
  */
 package org.springframework.data.mongodb;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /**
  * A specific {@link ClientSessionException} related to issues with a transaction such as aborted or non existing
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionManager.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionManager.java
index eda657f5f1..1f97bb69e9 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionManager.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionManager.java
@@ -15,8 +15,8 @@
  */
 package org.springframework.data.mongodb;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.beans.factory.InitializingBean;
-import org.springframework.lang.Nullable;
 import org.springframework.transaction.TransactionDefinition;
 import org.springframework.transaction.TransactionException;
 import org.springframework.transaction.TransactionSystemException;
@@ -36,19 +36,15 @@
 
 /**
  * A {@link org.springframework.transaction.PlatformTransactionManager} implementation that manages
- * {@link ClientSession} based transactions for a single {@link MongoDatabaseFactory}.
- * <br />
- * Binds a {@link ClientSession} from the specified {@link MongoDatabaseFactory} to the thread.
- * <br />
+ * {@link ClientSession} based transactions for a single {@link MongoDatabaseFactory}. <br />
+ * Binds a {@link ClientSession} from the specified {@link MongoDatabaseFactory} to the thread. <br />
  * {@link TransactionDefinition#isReadOnly() Readonly} transactions operate on a {@link ClientSession} and enable causal
  * consistency, and also {@link ClientSession#startTransaction() start}, {@link ClientSession#commitTransaction()
- * commit} or {@link ClientSession#abortTransaction() abort} a transaction.
- * <br />
+ * commit} or {@link ClientSession#abortTransaction() abort} a transaction. <br />
  * Application code is required to retrieve the {@link com.mongodb.client.MongoDatabase} via
  * {@link MongoDatabaseUtils#getDatabase(MongoDatabaseFactory)} instead of a standard
  * {@link MongoDatabaseFactory#getMongoDatabase()} call. Spring classes such as
- * {@link org.springframework.data.mongodb.core.MongoTemplate} use this strategy implicitly.
- * <br />
+ * {@link org.springframework.data.mongodb.core.MongoTemplate} use this strategy implicitly. <br />
  * By default failure of a {@literal commit} operation raises a {@link TransactionSystemException}. One may override
  * {@link #doCommit(MongoTransactionObject)} to implement the
  * <a href="https://docs.mongodb.com/manual/core/transactions/#retry-commit-operation">Retry Commit Operation</a>
@@ -80,7 +76,9 @@ public class MongoTransactionManager extends AbstractPlatformTransactionManager
 	 * @see #setTransactionSynchronization(int)
 	 */
 	public MongoTransactionManager() {
+
 		this.transactionOptionsResolver = MongoTransactionOptionsResolver.defaultResolver();
+		this.options = MongoTransactionOptions.NONE;
 	}
 
 	/**
@@ -151,7 +149,8 @@ protected void doBegin(Object transaction, TransactionDefinition definition) thr
 		}
 
 		try {
-			MongoTransactionOptions mongoTransactionOptions = transactionOptionsResolver.resolve(definition).mergeWith(options);
+			MongoTransactionOptions mongoTransactionOptions = transactionOptionsResolver.resolve(definition)
+					.mergeWith(options);
 			mongoTransactionObject.startTransaction(mongoTransactionOptions.toDriverOptions());
 		} catch (MongoException ex) {
 			throw new TransactionSystemException(String.format("Could not start Mongo transaction for session %s.",
@@ -206,6 +205,7 @@ protected final void doCommit(DefaultTransactionStatus status) throws Transactio
 	 * By default those labels are ignored, nevertheless one might check for
 	 * {@link MongoException#UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL transient commit errors labels} and retry the the
 	 * commit. <br />
+	 * 
 	 * <pre>
 	 * <code>
 	 * int retries = 3;
@@ -302,8 +302,7 @@ public void setOptions(@Nullable TransactionOptions options) {
 	 *
 	 * @return can be {@literal null}.
 	 */
-	@Nullable
-	public MongoDatabaseFactory getDatabaseFactory() {
+	public @Nullable MongoDatabaseFactory getDatabaseFactory() {
 		return databaseFactory;
 	}
 
@@ -461,8 +460,7 @@ void closeSession() {
 			}
 		}
 
-		@Nullable
-		public ClientSession getSession() {
+		public @Nullable ClientSession getSession() {
 			return resourceHolder != null ? resourceHolder.getSession() : null;
 		}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptions.java
index e411bd5d2d..04bcd36e35 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptions.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptions.java
@@ -19,15 +19,16 @@
 import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.ReadConcernAware;
 import org.springframework.data.mongodb.core.ReadPreferenceAware;
 import org.springframework.data.mongodb.core.WriteConcernAware;
-import org.springframework.lang.Nullable;
 
 import com.mongodb.ReadConcern;
 import com.mongodb.ReadPreference;
 import com.mongodb.TransactionOptions;
 import com.mongodb.WriteConcern;
+import org.springframework.lang.Contract;
 
 /**
  * Options to be applied within a specific transaction scope.
@@ -43,27 +44,23 @@ public interface MongoTransactionOptions
 	 */
 	MongoTransactionOptions NONE = new MongoTransactionOptions() {
 
-		@Nullable
 		@Override
-		public Duration getMaxCommitTime() {
+		public @Nullable Duration getMaxCommitTime() {
 			return null;
 		}
 
-		@Nullable
 		@Override
-		public ReadConcern getReadConcern() {
+		public @Nullable ReadConcern getReadConcern() {
 			return null;
 		}
 
-		@Nullable
 		@Override
-		public ReadPreference getReadPreference() {
+		public @Nullable ReadPreference getReadPreference() {
 			return null;
 		}
 
-		@Nullable
 		@Override
-		public WriteConcern getWriteConcern() {
+		public @Nullable WriteConcern getWriteConcern() {
 			return null;
 		}
 	};
@@ -76,6 +73,7 @@ public WriteConcern getWriteConcern() {
 	 * @return new instance of {@link MongoTransactionOptions} or this if {@literal fallbackOptions} is {@literal null} or
 	 *         {@link #NONE}.
 	 */
+	@Contract("null -> this")
 	default MongoTransactionOptions mergeWith(@Nullable MongoTransactionOptions fallbackOptions) {
 
 		if (fallbackOptions == null || MongoTransactionOptions.NONE.equals(fallbackOptions)) {
@@ -84,30 +82,26 @@ default MongoTransactionOptions mergeWith(@Nullable MongoTransactionOptions fall
 
 		return new MongoTransactionOptions() {
 
-			@Nullable
 			@Override
-			public Duration getMaxCommitTime() {
+			public @Nullable Duration getMaxCommitTime() {
 				return MongoTransactionOptions.this.hasMaxCommitTime() ? MongoTransactionOptions.this.getMaxCommitTime()
 						: fallbackOptions.getMaxCommitTime();
 			}
 
-			@Nullable
 			@Override
-			public ReadConcern getReadConcern() {
+			public @Nullable ReadConcern getReadConcern() {
 				return MongoTransactionOptions.this.hasReadConcern() ? MongoTransactionOptions.this.getReadConcern()
 						: fallbackOptions.getReadConcern();
 			}
 
-			@Nullable
 			@Override
-			public ReadPreference getReadPreference() {
+			public @Nullable ReadPreference getReadPreference() {
 				return MongoTransactionOptions.this.hasReadPreference() ? MongoTransactionOptions.this.getReadPreference()
 						: fallbackOptions.getReadPreference();
 			}
 
-			@Nullable
 			@Override
-			public WriteConcern getWriteConcern() {
+			public @Nullable WriteConcern getWriteConcern() {
 				return MongoTransactionOptions.this.hasWriteConcern() ? MongoTransactionOptions.this.getWriteConcern()
 						: fallbackOptions.getWriteConcern();
 			}
@@ -128,8 +122,8 @@ default <T> T map(Function<MongoTransactionOptions, T> mappingFunction) {
 	 * @return MongoDB driver native {@link TransactionOptions}.
 	 * @see MongoTransactionOptions#map(Function)
 	 */
-	@Nullable
-	default TransactionOptions toDriverOptions() {
+	@SuppressWarnings("NullAway")
+	default @Nullable TransactionOptions toDriverOptions() {
 
 		return map(it -> {
 
@@ -157,7 +151,7 @@ default TransactionOptions toDriverOptions() {
 	/**
 	 * Factory method to wrap given MongoDB driver native {@link TransactionOptions} into {@link MongoTransactionOptions}.
 	 *
-	 * @param options
+	 * @param options can be {@literal null}.
 	 * @return {@link MongoTransactionOptions#NONE} if given object is {@literal null}.
 	 */
 	static MongoTransactionOptions of(@Nullable TransactionOptions options) {
@@ -168,35 +162,30 @@ static MongoTransactionOptions of(@Nullable TransactionOptions options) {
 
 		return new MongoTransactionOptions() {
 
-			@Nullable
 			@Override
-			public Duration getMaxCommitTime() {
+			public @Nullable Duration getMaxCommitTime() {
 
 				Long millis = options.getMaxCommitTime(TimeUnit.MILLISECONDS);
 				return millis != null ? Duration.ofMillis(millis) : null;
 			}
 
-			@Nullable
 			@Override
-			public ReadConcern getReadConcern() {
+			public @Nullable ReadConcern getReadConcern() {
 				return options.getReadConcern();
 			}
 
-			@Nullable
 			@Override
-			public ReadPreference getReadPreference() {
+			public @Nullable ReadPreference getReadPreference() {
 				return options.getReadPreference();
 			}
 
-			@Nullable
 			@Override
-			public WriteConcern getWriteConcern() {
+			public @Nullable WriteConcern getWriteConcern() {
 				return options.getWriteConcern();
 			}
 
-			@Nullable
 			@Override
-			public TransactionOptions toDriverOptions() {
+			public @Nullable TransactionOptions toDriverOptions() {
 				return options;
 			}
 		};
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptionsResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptionsResolver.java
index b73b079a99..c4bdbcca53 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptionsResolver.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptionsResolver.java
@@ -18,7 +18,7 @@
 import java.util.Map;
 import java.util.stream.Collectors;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.transaction.TransactionDefinition;
 import org.springframework.transaction.interceptor.TransactionAttribute;
 import org.springframework.util.Assert;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoDatabaseUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoDatabaseUtils.java
index f397818a4c..3d1c2ee89c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoDatabaseUtils.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoDatabaseUtils.java
@@ -18,7 +18,7 @@
 import reactor.core.publisher.Mono;
 import reactor.util.context.Context;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.transaction.NoTransactionException;
 import org.springframework.transaction.reactive.ReactiveResourceSynchronization;
 import org.springframework.transaction.reactive.TransactionSynchronization;
@@ -35,8 +35,7 @@
 /**
  * Helper class for managing reactive {@link MongoDatabase} instances via {@link ReactiveMongoDatabaseFactory}. Used for
  * obtaining {@link ClientSession session bound} resources, such as {@link MongoDatabase} and {@link MongoCollection}
- * suitable for transactional usage.
- * <br />
+ * suitable for transactional usage. <br />
  * <strong>Note:</strong> Intended for internal usage only.
  *
  * @author Mark Paluch
@@ -74,8 +73,7 @@ public static Mono<Boolean> isTransactionActive(ReactiveMongoDatabaseFactory dat
 
 	/**
 	 * Obtain the default {@link MongoDatabase database} form the given {@link ReactiveMongoDatabaseFactory factory} using
-	 * {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}.
-	 * <br />
+	 * {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}. <br />
 	 * Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the subscriber
 	 * {@link Context} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}.
 	 *
@@ -103,32 +101,32 @@ public static Mono<MongoDatabase> getDatabase(ReactiveMongoDatabaseFactory facto
 
 	/**
 	 * Obtain the {@link MongoDatabase database} with given name form the given {@link ReactiveMongoDatabaseFactory
-	 * factory} using {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}.
-	 * <br />
+	 * factory} using {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}. <br />
 	 * Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the subscriber
 	 * {@link Context} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}.
 	 *
-	 * @param dbName the name of the {@link MongoDatabase} to get.
+	 * @param dbName the name of the {@link MongoDatabase} to get. If {@literal null} the default database of the
+	 *          {@link ReactiveMongoDatabaseFactory}.
 	 * @param factory the {@link ReactiveMongoDatabaseFactory} to get the {@link MongoDatabase} from.
 	 * @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}.
 	 */
-	public static Mono<MongoDatabase> getDatabase(String dbName, ReactiveMongoDatabaseFactory factory) {
+	public static Mono<MongoDatabase> getDatabase(@Nullable String dbName, ReactiveMongoDatabaseFactory factory) {
 		return doGetMongoDatabase(dbName, factory, SessionSynchronization.ON_ACTUAL_TRANSACTION);
 	}
 
 	/**
 	 * Obtain the {@link MongoDatabase database} with given name form the given {@link ReactiveMongoDatabaseFactory
-	 * factory}.
-	 * <br />
+	 * factory}. <br />
 	 * Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the subscriber
 	 * {@link Context} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}.
 	 *
-	 * @param dbName the name of the {@link MongoDatabase} to get.
+	 * @param dbName the name of the {@link MongoDatabase} to get. If {@literal null} the default database of the *
+	 *          {@link ReactiveMongoDatabaseFactory}.
 	 * @param factory the {@link ReactiveMongoDatabaseFactory} to get the {@link MongoDatabase} from.
 	 * @param sessionSynchronization the synchronization to use. Must not be {@literal null}.
 	 * @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}.
 	 */
-	public static Mono<MongoDatabase> getDatabase(String dbName, ReactiveMongoDatabaseFactory factory,
+	public static Mono<MongoDatabase> getDatabase(@Nullable String dbName, ReactiveMongoDatabaseFactory factory,
 			SessionSynchronization sessionSynchronization) {
 		return doGetMongoDatabase(dbName, factory, sessionSynchronization);
 	}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoResourceHolder.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoResourceHolder.java
index 33caa5e7fe..d01364b202 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoResourceHolder.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoResourceHolder.java
@@ -15,16 +15,15 @@
  */
 package org.springframework.data.mongodb;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
-import org.springframework.lang.Nullable;
 import org.springframework.transaction.support.ResourceHolderSupport;
 
 import com.mongodb.reactivestreams.client.ClientSession;
 
 /**
  * MongoDB specific resource holder, wrapping a {@link ClientSession}. {@link ReactiveMongoTransactionManager} binds
- * instances of this class to the subscriber context.
- * <br />
+ * instances of this class to the subscriber context. <br />
  * <strong>Note:</strong> Intended for internal usage only.
  *
  * @author Mark Paluch
@@ -103,8 +102,7 @@ boolean hasSession() {
 	 * @param session
 	 * @return
 	 */
-	@Nullable
-	public ClientSession setSessionIfAbsent(@Nullable ClientSession session) {
+	public @Nullable ClientSession setSessionIfAbsent(@Nullable ClientSession session) {
 
 		if (!hasSession()) {
 			setSession(session);
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoTransactionManager.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoTransactionManager.java
index 2c65c26b79..4f293c8ed6 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoTransactionManager.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoTransactionManager.java
@@ -17,8 +17,8 @@
 
 import reactor.core.publisher.Mono;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.beans.factory.InitializingBean;
-import org.springframework.lang.Nullable;
 import org.springframework.transaction.TransactionDefinition;
 import org.springframework.transaction.TransactionException;
 import org.springframework.transaction.TransactionSystemException;
@@ -64,7 +64,7 @@
 public class ReactiveMongoTransactionManager extends AbstractReactiveTransactionManager implements InitializingBean {
 
 	private @Nullable ReactiveMongoDatabaseFactory databaseFactory;
-	private @Nullable MongoTransactionOptions options;
+	private MongoTransactionOptions options;
 	private final MongoTransactionOptionsResolver transactionOptionsResolver;
 
 	/**
@@ -79,7 +79,9 @@ public class ReactiveMongoTransactionManager extends AbstractReactiveTransaction
 	 * @see #setDatabaseFactory(ReactiveMongoDatabaseFactory)
 	 */
 	public ReactiveMongoTransactionManager() {
+
 		this.transactionOptionsResolver = MongoTransactionOptionsResolver.defaultResolver();
+		this.options = MongoTransactionOptions.NONE;
 	}
 
 	/**
@@ -98,7 +100,7 @@ public ReactiveMongoTransactionManager(ReactiveMongoDatabaseFactory databaseFact
 	 * starting a new transaction.
 	 *
 	 * @param databaseFactory must not be {@literal null}.
-	 * @param options can be {@literal null}.
+	 * @param options can be {@literal null}. Will default {@link MongoTransactionOptions#NONE} if {@literal null}.
 	 */
 	public ReactiveMongoTransactionManager(ReactiveMongoDatabaseFactory databaseFactory,
 			@Nullable TransactionOptions options) {
@@ -112,7 +114,8 @@ public ReactiveMongoTransactionManager(ReactiveMongoDatabaseFactory databaseFact
 	 *
 	 * @param databaseFactory must not be {@literal null}.
 	 * @param transactionOptionsResolver must not be {@literal null}.
-	 * @param defaultTransactionOptions can be {@literal null}.
+	 * @param defaultTransactionOptions can be {@literal null}. Will default {@link MongoTransactionOptions#NONE} if
+	 *          {@literal null}.
 	 * @since 4.3
 	 */
 	public ReactiveMongoTransactionManager(ReactiveMongoDatabaseFactory databaseFactory,
@@ -124,7 +127,7 @@ public ReactiveMongoTransactionManager(ReactiveMongoDatabaseFactory databaseFact
 
 		this.databaseFactory = databaseFactory;
 		this.transactionOptionsResolver = transactionOptionsResolver;
-		this.options = defaultTransactionOptions;
+		this.options = defaultTransactionOptions != null ? defaultTransactionOptions : MongoTransactionOptions.NONE;
 	}
 
 	@Override
@@ -318,8 +321,7 @@ public void setOptions(@Nullable TransactionOptions options) {
 	 *
 	 * @return can be {@literal null}.
 	 */
-	@Nullable
-	public ReactiveMongoDatabaseFactory getDatabaseFactory() {
+	public @Nullable ReactiveMongoDatabaseFactory getDatabaseFactory() {
 		return databaseFactory;
 	}
 
@@ -470,8 +472,7 @@ void closeSession() {
 			}
 		}
 
-		@Nullable
-		public ClientSession getSession() {
+		public @Nullable ClientSession getSession() {
 			return resourceHolder != null ? resourceHolder.getSession() : null;
 		}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SessionAwareMethodInterceptor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SessionAwareMethodInterceptor.java
index 93dbf5db69..ec30478a54 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SessionAwareMethodInterceptor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SessionAwareMethodInterceptor.java
@@ -22,8 +22,8 @@
 
 import org.aopalliance.intercept.MethodInterceptor;
 import org.aopalliance.intercept.MethodInvocation;
+import org.jspecify.annotations.Nullable;
 import org.springframework.core.MethodClassKey;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.ConcurrentReferenceHashMap;
@@ -34,8 +34,7 @@
 
 /**
  * {@link MethodInterceptor} implementation looking up and invoking an alternative target method having
- * {@link ClientSession} as its first argument. This allows seamless integration with the existing code base.
- * <br />
+ * {@link ClientSession} as its first argument. This allows seamless integration with the existing code base. <br />
  * The {@link MethodInterceptor} is aware of methods on {@code MongoCollection} that my return new instances of itself
  * like (eg. {@link com.mongodb.reactivestreams.client.MongoCollection#withWriteConcern(WriteConcern)} and decorate them
  * if not already proxied.
@@ -95,13 +94,13 @@ public <T> SessionAwareMethodInterceptor(ClientSession session, T target, Class<
 		this.sessionType = sessionType;
 	}
 
-	@Nullable
 	@Override
-	public Object invoke(MethodInvocation methodInvocation) throws Throwable {
+	public @Nullable Object invoke(MethodInvocation methodInvocation) throws Throwable {
 
 		if (requiresDecoration(methodInvocation.getMethod())) {
 
 			Object target = methodInvocation.proceed();
+			Assert.notNull(target, "invocation target was null");
 			if (target instanceof Proxy) {
 				return target;
 			}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SimpleMongoTransactionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SimpleMongoTransactionOptions.java
index b52fc0bd71..5c50ba686a 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SimpleMongoTransactionOptions.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SimpleMongoTransactionOptions.java
@@ -21,7 +21,7 @@
 import java.util.Set;
 import java.util.stream.Collectors;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.Assert;
 
 import com.mongodb.Function;
@@ -41,10 +41,10 @@ class SimpleMongoTransactionOptions implements MongoTransactionOptions {
 	static final Set<String> KNOWN_KEYS = Arrays.stream(OptionKey.values()).map(OptionKey::getKey)
 			.collect(Collectors.toSet());
 
-	private final Duration maxCommitTime;
-	private final ReadConcern readConcern;
-	private final ReadPreference readPreference;
-	private final WriteConcern writeConcern;
+	private final @Nullable Duration maxCommitTime;
+	private final @Nullable ReadConcern readConcern;
+	private final @Nullable ReadPreference readPreference;
+	private final @Nullable WriteConcern writeConcern;
 
 	static SimpleMongoTransactionOptions of(Map<String, String> options) {
 		return new SimpleMongoTransactionOptions(options);
@@ -58,27 +58,23 @@ private SimpleMongoTransactionOptions(Map<String, String> options) {
 		this.writeConcern = doGetWriteConcern(options);
 	}
 
-	@Nullable
 	@Override
-	public Duration getMaxCommitTime() {
+	public @Nullable Duration getMaxCommitTime() {
 		return maxCommitTime;
 	}
 
-	@Nullable
 	@Override
-	public ReadConcern getReadConcern() {
+	public @Nullable ReadConcern getReadConcern() {
 		return readConcern;
 	}
 
-	@Nullable
 	@Override
-	public ReadPreference getReadPreference() {
+	public @Nullable ReadPreference getReadPreference() {
 		return readPreference;
 	}
 
-	@Nullable
 	@Override
-	public WriteConcern getWriteConcern() {
+	public @Nullable WriteConcern getWriteConcern() {
 		return writeConcern;
 	}
 
@@ -89,8 +85,7 @@ public String toString() {
 				+ ", readPreference=" + readPreference + ", writeConcern=" + writeConcern + '}';
 	}
 
-	@Nullable
-	private static Duration doGetMaxCommitTime(Map<String, String> options) {
+	private static @Nullable Duration doGetMaxCommitTime(Map<String, String> options) {
 
 		return getValue(options, OptionKey.MAX_COMMIT_TIME, value -> {
 
@@ -100,18 +95,15 @@ private static Duration doGetMaxCommitTime(Map<String, String> options) {
 		});
 	}
 
-	@Nullable
-	private static ReadConcern doGetReadConcern(Map<String, String> options) {
+	private static @Nullable ReadConcern doGetReadConcern(Map<String, String> options) {
 		return getValue(options, OptionKey.READ_CONCERN, value -> new ReadConcern(ReadConcernLevel.fromString(value)));
 	}
 
-	@Nullable
-	private static ReadPreference doGetReadPreference(Map<String, String> options) {
+	private static @Nullable ReadPreference doGetReadPreference(Map<String, String> options) {
 		return getValue(options, OptionKey.READ_PREFERENCE, ReadPreference::valueOf);
 	}
 
-	@Nullable
-	private static WriteConcern doGetWriteConcern(Map<String, String> options) {
+	private static @Nullable WriteConcern doGetWriteConcern(Map<String, String> options) {
 
 		return getValue(options, OptionKey.WRITE_CONCERN, value -> {
 
@@ -123,8 +115,8 @@ private static WriteConcern doGetWriteConcern(Map<String, String> options) {
 		});
 	}
 
-	@Nullable
-	private static <T> T getValue(Map<String, String> options, OptionKey key, Function<String, T> convertFunction) {
+	private static <T> @Nullable T getValue(Map<String, String> options, OptionKey key,
+			Function<String, T> convertFunction) {
 
 		String value = options.get(key.getKey());
 		return value != null ? convertFunction.apply(value) : null;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionMetadata.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionMetadata.java
index cd5f58d5b1..57ecec0342 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionMetadata.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionMetadata.java
@@ -17,7 +17,7 @@
 
 import java.time.Duration;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /**
  * MongoDB-specific transaction metadata.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionOptionResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionOptionResolver.java
index 37c7e3686b..e42c26d95a 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionOptionResolver.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionOptionResolver.java
@@ -15,7 +15,7 @@
  */
 package org.springframework.data.mongodb;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.transaction.TransactionDefinition;
 
 /**
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/UncategorizedMongoDbException.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/UncategorizedMongoDbException.java
index bec05d0d68..69ec086e5a 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/UncategorizedMongoDbException.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/UncategorizedMongoDbException.java
@@ -15,8 +15,8 @@
  */
 package org.springframework.data.mongodb;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.dao.UncategorizedDataAccessException;
-import org.springframework.lang.Nullable;
 
 public class UncategorizedMongoDbException extends UncategorizedDataAccessException {
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotPredicates.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotPredicates.java
index 2fe27a2c9e..86a70600a8 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotPredicates.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotPredicates.java
@@ -17,11 +17,11 @@
 
 import java.util.function.Predicate;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
 import org.springframework.data.util.ReactiveWrappers;
 import org.springframework.data.util.ReactiveWrappers.ReactiveLibrary;
 import org.springframework.data.util.TypeUtils;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ClassUtils;
 
 /**
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoManagedTypesBeanRegistrationAotProcessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoManagedTypesBeanRegistrationAotProcessor.java
index a33f20ffb6..4b7aa10c3f 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoManagedTypesBeanRegistrationAotProcessor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoManagedTypesBeanRegistrationAotProcessor.java
@@ -15,11 +15,11 @@
  */
 package org.springframework.data.mongodb.aot;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.aot.generate.GenerationContext;
 import org.springframework.core.ResolvableType;
 import org.springframework.data.aot.ManagedTypesBeanRegistrationAotProcessor;
 import org.springframework.data.mongodb.MongoManagedTypes;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ClassUtils;
 
 /**
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java
index 538fe4e812..f2442960ed 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java
@@ -15,10 +15,11 @@
  */
 package org.springframework.data.mongodb.aot;
 
-import static org.springframework.data.mongodb.aot.MongoAotPredicates.*;
+import static org.springframework.data.mongodb.aot.MongoAotPredicates.isReactorPresent;
 
 import java.util.Arrays;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.aot.hint.MemberCategory;
 import org.springframework.aot.hint.RuntimeHints;
 import org.springframework.aot.hint.RuntimeHintsRegistrar;
@@ -31,7 +32,6 @@
 import org.springframework.data.mongodb.core.mapping.event.ReactiveAfterSaveCallback;
 import org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeConvertCallback;
 import org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeSaveCallback;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ClassUtils;
 
 import com.mongodb.MongoClientSettings;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ConnectionStringPropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ConnectionStringPropertyEditor.java
index b070a0190f..0f6ba01704 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ConnectionStringPropertyEditor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ConnectionStringPropertyEditor.java
@@ -17,7 +17,7 @@
 
 import java.beans.PropertyEditorSupport;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.StringUtils;
 
 import com.mongodb.ConnectionString;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java
index 164b4defb6..f3a7dc0437 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java
@@ -21,6 +21,8 @@
 import java.util.List;
 import java.util.Set;
 
+import org.jspecify.annotations.NullUnmarked;
+import org.jspecify.annotations.Nullable;
 import org.springframework.beans.BeanMetadataElement;
 import org.springframework.beans.factory.config.BeanDefinition;
 import org.springframework.beans.factory.config.BeanDefinitionHolder;
@@ -56,7 +58,6 @@
 import org.springframework.data.mongodb.core.mapping.Document;
 import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
 import org.springframework.data.mongodb.core.mapping.event.ValidatingMongoEventListener;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.ObjectUtils;
@@ -76,6 +77,7 @@
  * @author Zied Yaich
  * @author Tomasz Forys
  */
+@NullUnmarked
 public class MappingMongoConverterParser implements BeanDefinitionParser {
 
 	private static final String BASE_PACKAGE = "base-package";
@@ -157,8 +159,7 @@ public BeanDefinition parse(Element element, ParserContext parserContext) {
 		return null;
 	}
 
-	@Nullable
-	private BeanDefinition potentiallyCreateValidatingMongoEventListener(Element element, ParserContext parserContext) {
+	private @Nullable BeanDefinition potentiallyCreateValidatingMongoEventListener(Element element, ParserContext parserContext) {
 
 		String disableValidation = element.getAttribute("disable-validation");
 		boolean validationDisabled = StringUtils.hasText(disableValidation) && Boolean.parseBoolean(disableValidation);
@@ -291,8 +292,7 @@ private static void parseFieldNamingStrategy(Element element, ReaderContext cont
 		}
 	}
 
-	@Nullable
-	private BeanDefinition getCustomConversions(Element element, ParserContext parserContext) {
+	private @Nullable BeanDefinition getCustomConversions(Element element, ParserContext parserContext) {
 
 		List<Element> customConvertersElements = DomUtils.getChildElementsByTagName(element, "custom-converters");
 
@@ -354,8 +354,7 @@ private static Set<String> getInitialEntityClasses(Element element) {
 		return classes;
 	}
 
-	@Nullable
-	public BeanMetadataElement parseConverter(Element element, ParserContext parserContext) {
+	public @Nullable BeanMetadataElement parseConverter(Element element, ParserContext parserContext) {
 
 		String converterRef = element.getAttribute("ref");
 		if (StringUtils.hasText(converterRef)) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingBeanDefinitionParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingBeanDefinitionParser.java
index 4e05fe6c39..a304199776 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingBeanDefinitionParser.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingBeanDefinitionParser.java
@@ -18,6 +18,8 @@
 import static org.springframework.data.config.ParsingUtils.*;
 import static org.springframework.data.mongodb.config.BeanNames.*;
 
+import org.jspecify.annotations.NullUnmarked;
+import org.jspecify.annotations.Nullable;
 import org.springframework.beans.factory.support.AbstractBeanDefinition;
 import org.springframework.beans.factory.support.BeanDefinitionBuilder;
 import org.springframework.beans.factory.support.BeanDefinitionRegistry;
@@ -29,7 +31,6 @@
 import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
 import org.springframework.data.mongodb.core.mapping.event.AuditingEntityCallback;
 import org.springframework.data.mongodb.core.mapping.event.ReactiveAuditingEntityCallback;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.StringUtils;
 
@@ -42,6 +43,7 @@
  * @author Oliver Gierke
  * @author Mark Paluch
  */
+@NullUnmarked
 public class MongoAuditingBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
 
 	private static boolean PROJECT_REACTOR_AVAILABLE = ClassUtils.isPresent("reactor.core.publisher.Mono",
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoConfigurationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoConfigurationSupport.java
index 0594f6176c..b01827d8c6 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoConfigurationSupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoConfigurationSupport.java
@@ -35,6 +35,7 @@
 import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter;
 import org.springframework.data.mongodb.core.mapping.Document;
 import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
+import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.StringUtils;
 
@@ -52,7 +53,7 @@ public abstract class MongoConfigurationSupport {
 	/**
 	 * Return the name of the database to connect to.
 	 *
-	 * @return must not be {@literal null}.
+	 * @return never {@literal null}.
 	 */
 	protected abstract String getDatabaseName();
 
@@ -76,7 +77,7 @@ protected Collection<String> getMappingBasePackages() {
 	 * Creates a {@link MongoMappingContext} equipped with entity classes scanned from the mapping base package.
 	 *
 	 * @see #getMappingBasePackages()
-	 * @return
+	 * @return never {@literal null}.
 	 */
 	@Bean
 	public MongoMappingContext mongoMappingContext(MongoCustomConversions customConversions,
@@ -172,8 +173,10 @@ protected Set<Class<?>> scanForEntities(String basePackage) throws ClassNotFound
 
 			for (BeanDefinition candidate : componentProvider.findCandidateComponents(basePackage)) {
 
-				initialEntitySet
-						.add(ClassUtils.forName(candidate.getBeanClassName(), MongoConfigurationSupport.class.getClassLoader()));
+				String beanClassName = candidate.getBeanClassName();
+				Assert.notNull(beanClassName, "BeanClassName cannot be null");
+
+				initialEntitySet.add(ClassUtils.forName(beanClassName, MongoConfigurationSupport.class.getClassLoader()));
 			}
 		}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoCredentialPropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoCredentialPropertyEditor.java
index b8f23a35af..93d778c861 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoCredentialPropertyEditor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoCredentialPropertyEditor.java
@@ -26,7 +26,7 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.ReflectionUtils;
 import org.springframework.util.StringUtils;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoDbFactoryParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoDbFactoryParser.java
index 2e733cc79f..2d3649c53a 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoDbFactoryParser.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoDbFactoryParser.java
@@ -20,6 +20,8 @@
 
 import java.util.Set;
 
+import org.jspecify.annotations.NullUnmarked;
+import org.jspecify.annotations.Nullable;
 import org.springframework.beans.factory.BeanDefinitionStoreException;
 import org.springframework.beans.factory.config.BeanDefinition;
 import org.springframework.beans.factory.parsing.BeanComponentDefinition;
@@ -31,7 +33,6 @@
 import org.springframework.data.config.BeanComponentDefinitionBuilder;
 import org.springframework.data.mongodb.core.MongoClientFactoryBean;
 import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory;
-import org.springframework.lang.Nullable;
 import org.springframework.util.StringUtils;
 import org.w3c.dom.Element;
 
@@ -47,6 +48,7 @@
  * @author Viktor Khoroshko
  * @author Mark Paluch
  */
+@NullUnmarked
 public class MongoDbFactoryParser extends AbstractBeanDefinitionParser {
 
 	private static final Set<String> MONGO_URI_ALLOWED_ADDITIONAL_ATTRIBUTES = Set.of("id", "write-concern");
@@ -125,8 +127,7 @@ private BeanDefinition registerMongoBeanDefinition(Element element, ParserContex
 	 * @param parserContext
 	 * @return {@literal null} in case no client-/uri defined.
 	 */
-	@Nullable
-	private BeanDefinition getConnectionString(Element element, ParserContext parserContext) {
+	private @Nullable BeanDefinition getConnectionString(Element element, ParserContext parserContext) {
 
 		String type = null;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoJmxParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoJmxParser.java
deleted file mode 100644
index af1ffbbb02..0000000000
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoJmxParser.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright 2011-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.mongodb.config;
-
-import org.springframework.beans.factory.config.BeanDefinition;
-import org.springframework.beans.factory.parsing.BeanComponentDefinition;
-import org.springframework.beans.factory.parsing.CompositeComponentDefinition;
-import org.springframework.beans.factory.support.BeanDefinitionBuilder;
-import org.springframework.beans.factory.xml.BeanDefinitionParser;
-import org.springframework.beans.factory.xml.ParserContext;
-import org.springframework.data.mongodb.core.MongoAdmin;
-import org.springframework.data.mongodb.monitor.*;
-import org.springframework.util.StringUtils;
-import org.w3c.dom.Element;
-
-/**
- * @author Mark Pollack
- * @author Thomas Risberg
- * @author John Brisbin
- * @author Oliver Gierke
- * @author Christoph Strobl
- * @deprecated since 4.5
- */
-@Deprecated(since = "4.5", forRemoval = true)
-public class MongoJmxParser implements BeanDefinitionParser {
-
-	public BeanDefinition parse(Element element, ParserContext parserContext) {
-		String name = element.getAttribute("mongo-ref");
-		if (!StringUtils.hasText(name)) {
-			name = BeanNames.MONGO_BEAN_NAME;
-		}
-		registerJmxComponents(name, element, parserContext);
-		return null;
-	}
-
-	protected void registerJmxComponents(String mongoRefName, Element element, ParserContext parserContext) {
-		Object eleSource = parserContext.extractSource(element);
-
-		CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), eleSource);
-
-		createBeanDefEntry(AssertMetrics.class, compositeDef, mongoRefName, eleSource, parserContext);
-		createBeanDefEntry(BackgroundFlushingMetrics.class, compositeDef, mongoRefName, eleSource, parserContext);
-		createBeanDefEntry(BtreeIndexCounters.class, compositeDef, mongoRefName, eleSource, parserContext);
-		createBeanDefEntry(ConnectionMetrics.class, compositeDef, mongoRefName, eleSource, parserContext);
-		createBeanDefEntry(GlobalLockMetrics.class, compositeDef, mongoRefName, eleSource, parserContext);
-		createBeanDefEntry(MemoryMetrics.class, compositeDef, mongoRefName, eleSource, parserContext);
-		createBeanDefEntry(OperationCounters.class, compositeDef, mongoRefName, eleSource, parserContext);
-		createBeanDefEntry(ServerInfo.class, compositeDef, mongoRefName, eleSource, parserContext);
-		createBeanDefEntry(MongoAdmin.class, compositeDef, mongoRefName, eleSource, parserContext);
-
-		parserContext.registerComponent(compositeDef);
-
-	}
-
-	protected void createBeanDefEntry(Class<?> clazz, CompositeComponentDefinition compositeDef, String mongoRefName,
-			Object eleSource, ParserContext parserContext) {
-		BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
-		builder.getRawBeanDefinition().setSource(eleSource);
-		builder.addConstructorArgReference(mongoRefName);
-		BeanDefinition assertDef = builder.getBeanDefinition();
-		String assertName = parserContext.getReaderContext().registerWithGeneratedName(assertDef);
-		compositeDef.addNestedComponent(new BeanComponentDefinition(assertDef, assertName));
-	}
-
-}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoNamespaceHandler.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoNamespaceHandler.java
index 47519ca615..62a4a1082d 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoNamespaceHandler.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoNamespaceHandler.java
@@ -31,7 +31,6 @@ public void init() {
 		registerBeanDefinitionParser("mapping-converter", new MappingMongoConverterParser());
 		registerBeanDefinitionParser("mongo-client", new MongoClientParser());
 		registerBeanDefinitionParser("db-factory", new MongoDbFactoryParser());
-		registerBeanDefinitionParser("jmx", new MongoJmxParser());
 		registerBeanDefinitionParser("auditing", new MongoAuditingBeanDefinitionParser());
 		registerBeanDefinitionParser("template", new MongoTemplateParser());
 		registerBeanDefinitionParser("gridFsTemplate", new GridFsTemplateParser());
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoParsingUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoParsingUtils.java
index 95b56b58f3..00e993fdc8 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoParsingUtils.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoParsingUtils.java
@@ -19,6 +19,7 @@
 
 import java.util.Map;
 
+import org.jspecify.annotations.NullUnmarked;
 import org.springframework.beans.factory.config.BeanDefinition;
 import org.springframework.beans.factory.config.CustomEditorConfigurer;
 import org.springframework.beans.factory.support.BeanDefinitionBuilder;
@@ -40,6 +41,7 @@
  * @author Christoph Strobl
  * @author Mark Paluch
  */
+@NullUnmarked
 abstract class MongoParsingUtils {
 
 	private MongoParsingUtils() {}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoTemplateParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoTemplateParser.java
index 1e1b11356f..5053e540fe 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoTemplateParser.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoTemplateParser.java
@@ -18,6 +18,7 @@
 import static org.springframework.data.config.ParsingUtils.*;
 import static org.springframework.data.mongodb.config.MongoParsingUtils.*;
 
+import org.jspecify.annotations.NullUnmarked;
 import org.springframework.beans.factory.BeanDefinitionStoreException;
 import org.springframework.beans.factory.config.BeanDefinition;
 import org.springframework.beans.factory.parsing.BeanComponentDefinition;
@@ -37,6 +38,7 @@
  * @author Martin Baumgartner
  * @author Oliver Gierke
  */
+@NullUnmarked
 class MongoTemplateParser extends AbstractBeanDefinitionParser {
 
 	@Override
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadConcernPropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadConcernPropertyEditor.java
index 60bf126ae7..3f5cb0ca62 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadConcernPropertyEditor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadConcernPropertyEditor.java
@@ -17,7 +17,7 @@
 
 import java.beans.PropertyEditorSupport;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.StringUtils;
 
 import com.mongodb.ReadConcern;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadPreferencePropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadPreferencePropertyEditor.java
index 5ed9b66619..f24c435348 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadPreferencePropertyEditor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadPreferencePropertyEditor.java
@@ -17,10 +17,10 @@
 
 import java.beans.PropertyEditorSupport;
 
-import org.springframework.lang.Nullable;
-
 import com.mongodb.ReadPreference;
 
+import org.jspecify.annotations.Nullable;
+
 /**
  * Parse a {@link String} to a {@link ReadPreference}.
  *
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ServerAddressPropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ServerAddressPropertyEditor.java
index 9c51900902..9ff59e5b22 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ServerAddressPropertyEditor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ServerAddressPropertyEditor.java
@@ -23,7 +23,7 @@
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 
@@ -80,11 +80,10 @@ public void setAsText(@Nullable String replicaSetString) {
 	 * @param source
 	 * @return the
 	 */
-	@Nullable
-	private ServerAddress parseServerAddress(String source) {
+	private @Nullable ServerAddress parseServerAddress(String source) {
 
 		if (!StringUtils.hasText(source)) {
-			if(LOG.isWarnEnabled()) {
+			if (LOG.isWarnEnabled()) {
 				LOG.warn(String.format(COULD_NOT_PARSE_ADDRESS_MESSAGE, "source", source));
 			}
 			return null;
@@ -93,7 +92,7 @@ private ServerAddress parseServerAddress(String source) {
 		String[] hostAndPort = extractHostAddressAndPort(source.trim());
 
 		if (hostAndPort.length > 2) {
-			if(LOG.isWarnEnabled()) {
+			if (LOG.isWarnEnabled()) {
 				LOG.warn(String.format(COULD_NOT_PARSE_ADDRESS_MESSAGE, "source", source));
 			}
 			return null;
@@ -105,11 +104,11 @@ private ServerAddress parseServerAddress(String source) {
 
 			return port == null ? new ServerAddress(hostAddress) : new ServerAddress(hostAddress, port);
 		} catch (UnknownHostException e) {
-			if(LOG.isWarnEnabled()) {
+			if (LOG.isWarnEnabled()) {
 				LOG.warn(String.format(COULD_NOT_PARSE_ADDRESS_MESSAGE, "host", hostAndPort[0]));
 			}
 		} catch (NumberFormatException e) {
-			if(LOG.isWarnEnabled()) {
+			if (LOG.isWarnEnabled()) {
 				LOG.warn(String.format(COULD_NOT_PARSE_ADDRESS_MESSAGE, "port", hostAndPort[1]));
 			}
 		}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/UUidRepresentationPropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/UUidRepresentationPropertyEditor.java
index b777969967..23c15102ac 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/UUidRepresentationPropertyEditor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/UUidRepresentationPropertyEditor.java
@@ -18,7 +18,7 @@
 import java.beans.PropertyEditorSupport;
 
 import org.bson.UuidRepresentation;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.StringUtils;
 
 /**
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/WriteConcernPropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/WriteConcernPropertyEditor.java
index ee0d09e555..32c19e24c3 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/WriteConcernPropertyEditor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/WriteConcernPropertyEditor.java
@@ -17,7 +17,7 @@
 
 import java.beans.PropertyEditorSupport;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.StringUtils;
 
 import com.mongodb.WriteConcern;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/package-info.java
index 5a1e5b725e..555cc9f66e 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/package-info.java
@@ -1,6 +1,6 @@
 /**
  * Spring XML namespace configuration for MongoDB specific repositories.
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 package org.springframework.data.mongodb.config;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/AggregationUtil.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/AggregationUtil.java
index a00d95a9ad..ec7c368eaf 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/AggregationUtil.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/AggregationUtil.java
@@ -18,7 +18,7 @@
 import java.util.List;
 
 import org.bson.Document;
-
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mapping.context.MappingContext;
 import org.springframework.data.mongodb.core.aggregation.Aggregation;
 import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext;
@@ -30,7 +30,6 @@
 import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
 import org.springframework.data.util.Lazy;
-import org.springframework.lang.Nullable;
 
 /**
  * Utility methods to map {@link org.springframework.data.mongodb.core.aggregation.Aggregation} pipeline definitions and
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamEvent.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamEvent.java
index 17b8835b7e..8a74ace28b 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamEvent.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamEvent.java
@@ -21,9 +21,9 @@
 import org.bson.BsonTimestamp;
 import org.bson.BsonValue;
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.convert.MongoConverter;
 import org.springframework.data.mongodb.core.messaging.Message;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.ObjectUtils;
 
@@ -78,8 +78,7 @@ public ChangeStreamEvent(@Nullable ChangeStreamDocument<Document> raw, Class<T>
 	 *
 	 * @return can be {@literal null}.
 	 */
-	@Nullable
-	public ChangeStreamDocument<Document> getRaw() {
+	public @Nullable ChangeStreamDocument<Document> getRaw() {
 		return raw;
 	}
 
@@ -88,10 +87,10 @@ public ChangeStreamDocument<Document> getRaw() {
 	 *
 	 * @return can be {@literal null}.
 	 */
-	@Nullable
-	public Instant getTimestamp() {
+	public @Nullable Instant getTimestamp() {
 
-		return getBsonTimestamp() != null ? converter.getConversionService().convert(raw.getClusterTime(), Instant.class)
+		return getBsonTimestamp() != null && raw != null
+				? converter.getConversionService().convert(raw.getClusterTime(), Instant.class)
 				: null;
 	}
 
@@ -111,8 +110,7 @@ public BsonTimestamp getBsonTimestamp() {
 	 *
 	 * @return can be {@literal null}.
 	 */
-	@Nullable
-	public BsonValue getResumeToken() {
+	public @Nullable BsonValue getResumeToken() {
 		return raw != null ? raw.getResumeToken() : null;
 	}
 
@@ -121,8 +119,7 @@ public BsonValue getResumeToken() {
 	 *
 	 * @return can be {@literal null}.
 	 */
-	@Nullable
-	public OperationType getOperationType() {
+	public @Nullable OperationType getOperationType() {
 		return raw != null ? raw.getOperationType() : null;
 	}
 
@@ -131,8 +128,7 @@ public OperationType getOperationType() {
 	 *
 	 * @return can be {@literal null}.
 	 */
-	@Nullable
-	public String getDatabaseName() {
+	public @Nullable String getDatabaseName() {
 		return raw != null ? raw.getNamespace().getDatabaseName() : null;
 	}
 
@@ -141,8 +137,7 @@ public String getDatabaseName() {
 	 *
 	 * @return can be {@literal null}.
 	 */
-	@Nullable
-	public String getCollectionName() {
+	public @Nullable String getCollectionName() {
 		return raw != null ? raw.getNamespace().getCollectionName() : null;
 	}
 
@@ -152,8 +147,7 @@ public String getCollectionName() {
 	 * @return {@literal null} when {@link #getRaw()} or {@link ChangeStreamDocument#getFullDocument()} is
 	 *         {@literal null}.
 	 */
-	@Nullable
-	public T getBody() {
+	public @Nullable T getBody() {
 
 		if (raw == null || raw.getFullDocument() == null) {
 			return null;
@@ -163,14 +157,14 @@ public T getBody() {
 	}
 
 	/**
-	 * Get the potentially converted {@link ChangeStreamDocument#getFullDocumentBeforeChange() document} before being changed.
+	 * Get the potentially converted {@link ChangeStreamDocument#getFullDocumentBeforeChange() document} before being
+	 * changed.
 	 *
 	 * @return {@literal null} when {@link #getRaw()} or {@link ChangeStreamDocument#getFullDocumentBeforeChange()} is
 	 *         {@literal null}.
 	 * @since 4.0
 	 */
-	@Nullable
-	public T getBodyBeforeChange() {
+	public @Nullable T getBodyBeforeChange() {
 
 		if (raw == null || raw.getFullDocumentBeforeChange() == null) {
 			return null;
@@ -189,6 +183,7 @@ private T getConvertedFullDocument(Document fullDocument) {
 		return (T) doGetConverted(fullDocument, CONVERTED_FULL_DOCUMENT_UPDATER);
 	}
 
+	@SuppressWarnings("NullAway")
 	private Object doGetConverted(Document fullDocument, AtomicReferenceFieldUpdater<ChangeStreamEvent, Object> updater) {
 
 		Object result = updater.get(this);
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamOptions.java
index aaee3b76af..9c99b0e01f 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamOptions.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamOptions.java
@@ -23,9 +23,10 @@
 import org.bson.BsonTimestamp;
 import org.bson.BsonValue;
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.Aggregation;
 import org.springframework.data.mongodb.core.query.Collation;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.ObjectUtils;
@@ -248,6 +249,7 @@ private ChangeStreamOptionsBuilder() {}
 		 * @param collation must not be {@literal null} nor {@literal empty}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public ChangeStreamOptionsBuilder collation(Collation collation) {
 
 			Assert.notNull(collation, "Collation must not be null nor empty");
@@ -257,14 +259,12 @@ public ChangeStreamOptionsBuilder collation(Collation collation) {
 		}
 
 		/**
-		 * Set the filter to apply.
-		 * <br />
+		 * Set the filter to apply. <br />
 		 * Fields on aggregation expression root level are prefixed to map to fields contained in
 		 * {@link ChangeStreamDocument#getFullDocument() fullDocument}. However {@literal operationType}, {@literal ns},
 		 * {@literal documentKey} and {@literal fullDocument} are reserved words that will be omitted, and therefore taken
 		 * as given, during the mapping procedure. You may want to have a look at the
-		 * <a href="https://docs.mongodb.com/manual/reference/change-events/">structure of Change Events</a>.
-		 * <br />
+		 * <a href="https://docs.mongodb.com/manual/reference/change-events/">structure of Change Events</a>. <br />
 		 * Use {@link org.springframework.data.mongodb.core.aggregation.TypedAggregation} to ensure filter expressions are
 		 * mapped to domain type fields.
 		 *
@@ -272,6 +272,7 @@ public ChangeStreamOptionsBuilder collation(Collation collation) {
 		 *          {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public ChangeStreamOptionsBuilder filter(Aggregation filter) {
 
 			Assert.notNull(filter, "Filter must not be null");
@@ -286,6 +287,7 @@ public ChangeStreamOptionsBuilder filter(Aggregation filter) {
 		 * @param filter must not be {@literal null} nor contain {@literal null} values.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public ChangeStreamOptionsBuilder filter(Document... filter) {
 
 			Assert.noNullElements(filter, "Filter must not contain null values");
@@ -301,6 +303,7 @@ public ChangeStreamOptionsBuilder filter(Document... filter) {
 		 * @param resumeToken must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public ChangeStreamOptionsBuilder resumeToken(BsonValue resumeToken) {
 
 			Assert.notNull(resumeToken, "ResumeToken must not be null");
@@ -330,6 +333,7 @@ public ChangeStreamOptionsBuilder returnFullDocumentOnUpdate() {
 		 * @param lookup must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public ChangeStreamOptionsBuilder fullDocumentLookup(FullDocument lookup) {
 
 			Assert.notNull(lookup, "Lookup must not be null");
@@ -345,6 +349,7 @@ public ChangeStreamOptionsBuilder fullDocumentLookup(FullDocument lookup) {
 		 * @return this.
 		 * @since 4.0
 		 */
+		@Contract("_ -> this")
 		public ChangeStreamOptionsBuilder fullDocumentBeforeChangeLookup(FullDocumentBeforeChange lookup) {
 
 			Assert.notNull(lookup, "Lookup must not be null");
@@ -358,7 +363,7 @@ public ChangeStreamOptionsBuilder fullDocumentBeforeChangeLookup(FullDocumentBef
 		 *
 		 * @return this.
 		 * @since 4.0
-		 * @see #fullDocumentBeforeChangeLookup(FullDocumentBeforeChange) 
+		 * @see #fullDocumentBeforeChangeLookup(FullDocumentBeforeChange)
 		 */
 		public ChangeStreamOptionsBuilder returnFullDocumentBeforeChange() {
 			return fullDocumentBeforeChangeLookup(FullDocumentBeforeChange.WHEN_AVAILABLE);
@@ -370,6 +375,7 @@ public ChangeStreamOptionsBuilder returnFullDocumentBeforeChange() {
 		 * @param resumeTimestamp must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public ChangeStreamOptionsBuilder resumeAt(Instant resumeTimestamp) {
 
 			Assert.notNull(resumeTimestamp, "ResumeTimestamp must not be null");
@@ -385,6 +391,7 @@ public ChangeStreamOptionsBuilder resumeAt(Instant resumeTimestamp) {
 		 * @return this.
 		 * @since 2.2
 		 */
+		@Contract("_ -> this")
 		public ChangeStreamOptionsBuilder resumeAt(BsonTimestamp resumeTimestamp) {
 
 			Assert.notNull(resumeTimestamp, "ResumeTimestamp must not be null");
@@ -400,6 +407,7 @@ public ChangeStreamOptionsBuilder resumeAt(BsonTimestamp resumeTimestamp) {
 		 * @return this.
 		 * @since 2.2
 		 */
+		@Contract("_ -> this")
 		public ChangeStreamOptionsBuilder resumeAfter(BsonValue resumeToken) {
 
 			resumeToken(resumeToken);
@@ -415,6 +423,7 @@ public ChangeStreamOptionsBuilder resumeAfter(BsonValue resumeToken) {
 		 * @return this.
 		 * @since 2.2
 		 */
+		@Contract("_ -> this")
 		public ChangeStreamOptionsBuilder startAfter(BsonValue resumeToken) {
 
 			resumeToken(resumeToken);
@@ -426,6 +435,7 @@ public ChangeStreamOptionsBuilder startAfter(BsonValue resumeToken) {
 		/**
 		 * @return the built {@link ChangeStreamOptions}
 		 */
+		@Contract("-> new")
 		public ChangeStreamOptions build() {
 
 			ChangeStreamOptions options = new ChangeStreamOptions();
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionCallback.java
index c142aca173..bf8be5ba69 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionCallback.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionCallback.java
@@ -16,8 +16,8 @@
 package org.springframework.data.mongodb.core;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.dao.DataAccessException;
-import org.springframework.lang.Nullable;
 
 import com.mongodb.MongoException;
 import com.mongodb.client.MongoCollection;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java
index d627ba2468..f4d1891703 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java
@@ -15,18 +15,35 @@
  */
 package org.springframework.data.mongodb.core;
 
+import java.nio.charset.StandardCharsets;
 import java.time.Duration;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Optional;
 import java.util.function.Function;
+import java.util.stream.StreamSupport;
 
+import org.bson.BsonBinary;
+import org.bson.BsonBinarySubType;
+import org.bson.BsonNull;
+import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.mapping.Field;
 import org.springframework.data.mongodb.core.query.Collation;
+import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty;
+import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.QueryableJsonSchemaProperty;
+import org.springframework.data.mongodb.core.schema.JsonSchemaProperty;
 import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
+import org.springframework.data.mongodb.core.schema.QueryCharacteristic;
 import org.springframework.data.mongodb.core.timeseries.Granularity;
 import org.springframework.data.mongodb.core.timeseries.GranularityDefinition;
 import org.springframework.data.mongodb.core.validation.Validator;
 import org.springframework.data.util.Optionals;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.CheckReturnValue;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 
@@ -41,6 +58,7 @@
  * @author Mark Paluch
  * @author Andreas Zink
  * @author Ben Foster
+ * @author Ross Lawley
  */
 public class CollectionOptions {
 
@@ -51,10 +69,12 @@ public class CollectionOptions {
 	private ValidationOptions validationOptions;
 	private @Nullable TimeSeriesOptions timeSeriesOptions;
 	private @Nullable CollectionChangeStreamOptions changeStreamOptions;
+	private @Nullable EncryptedFieldsOptions encryptedFieldsOptions;
 
 	private CollectionOptions(@Nullable Long size, @Nullable Long maxDocuments, @Nullable Boolean capped,
 			@Nullable Collation collation, ValidationOptions validationOptions, @Nullable TimeSeriesOptions timeSeriesOptions,
-			@Nullable CollectionChangeStreamOptions changeStreamOptions) {
+			@Nullable CollectionChangeStreamOptions changeStreamOptions,
+			@Nullable EncryptedFieldsOptions encryptedFieldsOptions) {
 
 		this.maxDocuments = maxDocuments;
 		this.size = size;
@@ -63,6 +83,7 @@ private CollectionOptions(@Nullable Long size, @Nullable Long maxDocuments, @Nul
 		this.validationOptions = validationOptions;
 		this.timeSeriesOptions = timeSeriesOptions;
 		this.changeStreamOptions = changeStreamOptions;
+		this.encryptedFieldsOptions = encryptedFieldsOptions;
 	}
 
 	/**
@@ -76,7 +97,7 @@ public static CollectionOptions just(Collation collation) {
 
 		Assert.notNull(collation, "Collation must not be null");
 
-		return new CollectionOptions(null, null, null, collation, ValidationOptions.none(), null, null);
+		return new CollectionOptions(null, null, null, collation, ValidationOptions.none(), null, null, null);
 	}
 
 	/**
@@ -86,7 +107,7 @@ public static CollectionOptions just(Collation collation) {
 	 * @since 2.0
 	 */
 	public static CollectionOptions empty() {
-		return new CollectionOptions(null, null, null, null, ValidationOptions.none(), null, null);
+		return new CollectionOptions(null, null, null, null, ValidationOptions.none(), null, null, null);
 	}
 
 	/**
@@ -127,6 +148,46 @@ public static CollectionOptions emitChangedRevisions() {
 		return empty().changeStream(CollectionChangeStreamOptions.preAndPostImages(true));
 	}
 
+	/**
+	 * Create new {@link CollectionOptions} with the given {@code encryptedFields}.
+	 *
+	 * @param encryptedFieldsOptions can be null
+	 * @return new instance of {@link CollectionOptions}.
+	 * @since 4.5.0
+	 */
+	@Contract("_ -> new")
+	@CheckReturnValue
+	public static CollectionOptions encryptedCollection(@Nullable EncryptedFieldsOptions encryptedFieldsOptions) {
+		return new CollectionOptions(null, null, null, null, ValidationOptions.NONE, null, null, encryptedFieldsOptions);
+	}
+
+	/**
+	 * Create new {@link CollectionOptions} reading encryption options from the given {@link MongoJsonSchema}.
+	 *
+	 * @param schema must not be {@literal null}.
+	 * @return new instance of {@link CollectionOptions}.
+	 * @since 4.5.0
+	 */
+	@Contract("_ -> new")
+	@CheckReturnValue
+	public static CollectionOptions encryptedCollection(MongoJsonSchema schema) {
+		return encryptedCollection(EncryptedFieldsOptions.fromSchema(schema));
+	}
+
+	/**
+	 * Create new {@link CollectionOptions} building encryption options in a fluent style.
+	 *
+	 * @param optionsFunction must not be {@literal null}.
+	 * @return new instance of {@link CollectionOptions}.
+	 * @since 4.5.0
+	 */
+	@Contract("_ -> new")
+	@CheckReturnValue
+	public static CollectionOptions encryptedCollection(
+			Function<EncryptedFieldsOptions, EncryptedFieldsOptions> optionsFunction) {
+		return encryptedCollection(optionsFunction.apply(new EncryptedFieldsOptions()));
+	}
+
 	/**
 	 * Create new {@link CollectionOptions} with already given settings and capped set to {@literal true}. <br />
 	 * <strong>NOTE:</strong> Using capped collections requires defining {@link #size(long)}.
@@ -136,7 +197,7 @@ public static CollectionOptions emitChangedRevisions() {
 	 */
 	public CollectionOptions capped() {
 		return new CollectionOptions(size, maxDocuments, true, collation, validationOptions, timeSeriesOptions,
-				changeStreamOptions);
+				changeStreamOptions, encryptedFieldsOptions);
 	}
 
 	/**
@@ -148,7 +209,7 @@ public CollectionOptions capped() {
 	 */
 	public CollectionOptions maxDocuments(long maxDocuments) {
 		return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
-				changeStreamOptions);
+				changeStreamOptions, encryptedFieldsOptions);
 	}
 
 	/**
@@ -160,7 +221,7 @@ public CollectionOptions maxDocuments(long maxDocuments) {
 	 */
 	public CollectionOptions size(long size) {
 		return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
-				changeStreamOptions);
+				changeStreamOptions, encryptedFieldsOptions);
 	}
 
 	/**
@@ -172,19 +233,19 @@ public CollectionOptions size(long size) {
 	 */
 	public CollectionOptions collation(@Nullable Collation collation) {
 		return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
-				changeStreamOptions);
+				changeStreamOptions, encryptedFieldsOptions);
 	}
 
 	/**
 	 * Create new {@link CollectionOptions} with already given settings and {@code validationOptions} set to given
 	 * {@link MongoJsonSchema}.
 	 *
-	 * @param schema can be {@literal null}.
+	 * @param schema must not be {@literal null}.
 	 * @return new {@link CollectionOptions}.
 	 * @since 2.1
 	 */
-	public CollectionOptions schema(@Nullable MongoJsonSchema schema) {
-		return validator(Validator.schema(schema));
+	public CollectionOptions schema(MongoJsonSchema schema) {
+		return validator(schema != null ? Validator.schema(schema) : null);
 	}
 
 	/**
@@ -293,7 +354,7 @@ public CollectionOptions validation(ValidationOptions validationOptions) {
 
 		Assert.notNull(validationOptions, "ValidationOptions must not be null");
 		return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
-				changeStreamOptions);
+				changeStreamOptions, encryptedFieldsOptions);
 	}
 
 	/**
@@ -307,7 +368,7 @@ public CollectionOptions timeSeries(TimeSeriesOptions timeSeriesOptions) {
 
 		Assert.notNull(timeSeriesOptions, "TimeSeriesOptions must not be null");
 		return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
-				changeStreamOptions);
+				changeStreamOptions, encryptedFieldsOptions);
 	}
 
 	/**
@@ -321,7 +382,22 @@ public CollectionOptions changeStream(CollectionChangeStreamOptions changeStream
 
 		Assert.notNull(changeStreamOptions, "ChangeStreamOptions must not be null");
 		return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
-				changeStreamOptions);
+				changeStreamOptions, encryptedFieldsOptions);
+	}
+
+	/**
+	 * Set the {@link EncryptedFieldsOptions} for collections using queryable encryption.
+	 *
+	 * @param encryptedFieldsOptions must not be {@literal null}.
+	 * @return new instance of {@link CollectionOptions}.
+	 */
+	@Contract("_ -> new")
+	@CheckReturnValue
+	public CollectionOptions encrypted(EncryptedFieldsOptions encryptedFieldsOptions) {
+
+		Assert.notNull(encryptedFieldsOptions, "EncryptedCollectionOptions must not be null");
+		return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
+				changeStreamOptions, encryptedFieldsOptions);
 	}
 
 	/**
@@ -392,14 +468,24 @@ public Optional<CollectionChangeStreamOptions> getChangeStreamOptions() {
 		return Optional.ofNullable(changeStreamOptions);
 	}
 
+	/**
+	 * Get the {@code encryptedFields} if available.
+	 *
+	 * @return {@link Optional#empty()} if not specified.
+	 * @since 4.5
+	 */
+	public Optional<EncryptedFieldsOptions> getEncryptedFieldsOptions() {
+		return Optional.ofNullable(encryptedFieldsOptions);
+	}
+
 	@Override
 	public String toString() {
 		return "CollectionOptions{" + "maxDocuments=" + maxDocuments + ", size=" + size + ", capped=" + capped
 				+ ", collation=" + collation + ", validationOptions=" + validationOptions + ", timeSeriesOptions="
-				+ timeSeriesOptions + ", changeStreamOptions=" + changeStreamOptions + ", disableValidation="
-				+ disableValidation() + ", strictValidation=" + strictValidation() + ", moderateValidation="
-				+ moderateValidation() + ", warnOnValidationError=" + warnOnValidationError() + ", failOnValidationError="
-				+ failOnValidationError() + '}';
+				+ timeSeriesOptions + ", changeStreamOptions=" + changeStreamOptions + ", encryptedCollectionOptions="
+				+ encryptedFieldsOptions + ", disableValidation=" + disableValidation() + ", strictValidation="
+				+ strictValidation() + ", moderateValidation=" + moderateValidation() + ", warnOnValidationError="
+				+ warnOnValidationError() + ", failOnValidationError=" + failOnValidationError() + '}';
 	}
 
 	@Override
@@ -431,7 +517,10 @@ public boolean equals(@Nullable Object o) {
 		if (!ObjectUtils.nullSafeEquals(timeSeriesOptions, that.timeSeriesOptions)) {
 			return false;
 		}
-		return ObjectUtils.nullSafeEquals(changeStreamOptions, that.changeStreamOptions);
+		if (!ObjectUtils.nullSafeEquals(changeStreamOptions, that.changeStreamOptions)) {
+			return false;
+		}
+		return ObjectUtils.nullSafeEquals(encryptedFieldsOptions, that.encryptedFieldsOptions);
 	}
 
 	@Override
@@ -443,6 +532,7 @@ public int hashCode() {
 		result = 31 * result + ObjectUtils.nullSafeHashCode(validationOptions);
 		result = 31 * result + ObjectUtils.nullSafeHashCode(timeSeriesOptions);
 		result = 31 * result + ObjectUtils.nullSafeHashCode(changeStreamOptions);
+		result = 31 * result + ObjectUtils.nullSafeHashCode(encryptedFieldsOptions);
 		return result;
 	}
 
@@ -461,7 +551,8 @@ public static class ValidationOptions {
 		private final @Nullable ValidationLevel validationLevel;
 		private final @Nullable ValidationAction validationAction;
 
-		public ValidationOptions(Validator validator, ValidationLevel validationLevel, ValidationAction validationAction) {
+		public ValidationOptions(@Nullable Validator validator, @Nullable ValidationLevel validationLevel,
+				@Nullable ValidationAction validationAction) {
 
 			this.validator = validator;
 			this.validationLevel = validationLevel;
@@ -483,6 +574,7 @@ public static ValidationOptions none() {
 		 * @param validator can be {@literal null}.
 		 * @return new instance of {@link ValidationOptions}.
 		 */
+		@Contract("_ -> new")
 		public ValidationOptions validator(@Nullable Validator validator) {
 			return new ValidationOptions(validator, validationLevel, validationAction);
 		}
@@ -493,6 +585,7 @@ public ValidationOptions validator(@Nullable Validator validator) {
 		 * @param validationLevel can be {@literal null}.
 		 * @return new instance of {@link ValidationOptions}.
 		 */
+		@Contract("_ -> new")
 		public ValidationOptions validationLevel(ValidationLevel validationLevel) {
 			return new ValidationOptions(validator, validationLevel, validationAction);
 		}
@@ -503,6 +596,7 @@ public ValidationOptions validationLevel(ValidationLevel validationLevel) {
 		 * @param validationAction can be {@literal null}.
 		 * @return new instance of {@link ValidationOptions}.
 		 */
+		@Contract("_ -> new")
 		public ValidationOptions validationAction(ValidationAction validationAction) {
 			return new ValidationOptions(validator, validationLevel, validationAction);
 		}
@@ -576,6 +670,188 @@ public int hashCode() {
 		}
 	}
 
+	/**
+	 * Encapsulation of Encryption options for collections.
+	 *
+	 * @author Christoph Strobl
+	 * @since 4.5
+	 */
+	public static class EncryptedFieldsOptions {
+
+		private static final EncryptedFieldsOptions NONE = new EncryptedFieldsOptions();
+
+		private final @Nullable MongoJsonSchema schema;
+		private final List<QueryableJsonSchemaProperty> queryableProperties;
+
+		EncryptedFieldsOptions() {
+			this(null, List.of());
+		}
+
+		private EncryptedFieldsOptions(@Nullable MongoJsonSchema schema,
+				List<QueryableJsonSchemaProperty> queryableProperties) {
+
+			this.schema = schema;
+			this.queryableProperties = queryableProperties;
+		}
+
+		/**
+		 * @return {@link EncryptedFieldsOptions#NONE}
+		 */
+		public static EncryptedFieldsOptions none() {
+			return NONE;
+		}
+
+		/**
+		 * @return new instance of {@link EncryptedFieldsOptions}.
+		 */
+		public static EncryptedFieldsOptions fromSchema(MongoJsonSchema schema) {
+			return new EncryptedFieldsOptions(schema, List.of());
+		}
+
+		/**
+		 * @return new instance of {@link EncryptedFieldsOptions}.
+		 */
+		public static EncryptedFieldsOptions fromProperties(List<QueryableJsonSchemaProperty> properties) {
+			return new EncryptedFieldsOptions(null, List.copyOf(properties));
+		}
+
+		/**
+		 * Add a new {@link QueryableJsonSchemaProperty queryable property} for the given source property.
+		 * <p>
+		 * Please note that, a given {@link JsonSchemaProperty} may override options from a given {@link MongoJsonSchema} if
+		 * set.
+		 *
+		 * @param property the queryable source - typically
+		 *          {@link org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty
+		 *          encrypted}.
+		 * @param characteristics the query options to set.
+		 * @return new instance of {@link EncryptedFieldsOptions}.
+		 */
+		@Contract("_, _ -> new")
+		@CheckReturnValue
+		public EncryptedFieldsOptions queryable(JsonSchemaProperty property, QueryCharacteristic... characteristics) {
+
+			List<QueryableJsonSchemaProperty> targetPropertyList = new ArrayList<>(queryableProperties.size() + 1);
+			targetPropertyList.addAll(queryableProperties);
+			targetPropertyList.add(JsonSchemaProperty.queryable(property, List.of(characteristics)));
+
+			return new EncryptedFieldsOptions(schema, targetPropertyList);
+		}
+
+		public Document toDocument() {
+			return new Document("fields", selectPaths());
+		}
+
+		private List<Document> selectPaths() {
+
+			Map<String, Document> fields = new LinkedHashMap<>();
+			for (Document field : fromSchema()) {
+				fields.put(field.get("path", String.class), field);
+			}
+			for (Document field : fromProperties()) {
+				fields.put(field.get("path", String.class), field);
+			}
+			return List.copyOf(fields.values());
+		}
+
+		private List<Document> fromProperties() {
+
+			if (queryableProperties.isEmpty()) {
+				return List.of();
+			}
+
+			List<Document> converted = new ArrayList<>(queryableProperties.size());
+			for (QueryableJsonSchemaProperty property : queryableProperties) {
+
+				Document field = new Document("path", property.getIdentifier());
+
+				if (!property.getTypes().isEmpty()) {
+					field.append("bsonType", property.getTypes().iterator().next().toBsonType().value());
+				}
+
+				if (property
+						.getTargetProperty() instanceof IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty encrypted) {
+					if (encrypted.getKeyId() != null) {
+						if (encrypted.getKeyId() instanceof String stringKey) {
+							field.append("keyId",
+									new BsonBinary(BsonBinarySubType.UUID_STANDARD, stringKey.getBytes(StandardCharsets.UTF_8)));
+						} else {
+							field.append("keyId", encrypted.getKeyId());
+						}
+					}
+				}
+
+				field.append("queries", StreamSupport.stream(property.getCharacteristics().spliterator(), false)
+						.map(QueryCharacteristic::toDocument).toList());
+
+				if (!field.containsKey("keyId")) {
+					field.append("keyId", BsonNull.VALUE);
+				}
+
+				converted.add(field);
+			}
+			return converted;
+		}
+
+		private List<Document> fromSchema() {
+
+			if (schema == null) {
+				return List.of();
+			}
+
+			Document root = schema.schemaDocument();
+			Map<String, Document> paths = new LinkedHashMap<>();
+			collectPaths(root, null, paths);
+
+			List<Document> fields = new ArrayList<>();
+			if (!paths.isEmpty()) {
+
+				for (Entry<String, Document> entry : paths.entrySet()) {
+					Document field = new Document("path", entry.getKey());
+					field.append("keyId", entry.getValue().getOrDefault("keyId", BsonNull.VALUE));
+					if (entry.getValue().containsKey("bsonType")) {
+						field.append("bsonType", entry.getValue().get("bsonType"));
+					}
+					field.put("queries", entry.getValue().get("queries"));
+					fields.add(field);
+				}
+			}
+
+			return fields;
+		}
+	}
+
+	private static void collectPaths(Document document, @Nullable String currentPath, Map<String, Document> paths) {
+
+		if (document.containsKey("type") && document.get("type").equals("object")) {
+			Object o = document.get("properties");
+			if (o == null) {
+				return;
+			}
+
+			if (o instanceof Document properties) {
+				for (Entry<String, Object> entry : properties.entrySet()) {
+					if (entry.getValue() instanceof Document nested) {
+
+						String path = currentPath == null ? entry.getKey() : (currentPath + "." + entry.getKey());
+						if (nested.containsKey("encrypt")) {
+							Document target = new Document(nested.get("encrypt", Document.class));
+							if (nested.containsKey("queries")) {
+								List<?> queries = nested.get("queries", List.class);
+								if (!queries.isEmpty() && queries.iterator().next() instanceof Document qd) {
+									target.putAll(qd);
+								}
+							}
+							paths.put(path, target);
+						} else {
+							collectPaths(nested, path, paths);
+						}
+					}
+				}
+			}
+		}
+	}
+
 	/**
 	 * Encapsulation of options applied to define collections change stream behaviour.
 	 *
@@ -677,6 +953,7 @@ public static TimeSeriesOptions timeSeries(String timeField) {
 		 * @param metaField must not be {@literal null}.
 		 * @return new instance of {@link TimeSeriesOptions}.
 		 */
+		@Contract("_ -> new")
 		public TimeSeriesOptions metaField(String metaField) {
 			return new TimeSeriesOptions(timeField, metaField, granularity, expireAfter);
 		}
@@ -688,6 +965,7 @@ public TimeSeriesOptions metaField(String metaField) {
 		 * @return new instance of {@link TimeSeriesOptions}.
 		 * @see Granularity
 		 */
+		@Contract("_ -> new")
 		public TimeSeriesOptions granularity(GranularityDefinition granularity) {
 			return new TimeSeriesOptions(timeField, metaField, granularity, expireAfter);
 		}
@@ -700,6 +978,7 @@ public TimeSeriesOptions granularity(GranularityDefinition granularity) {
 		 * @see com.mongodb.client.model.CreateCollectionOptions#expireAfter(long, java.util.concurrent.TimeUnit)
 		 * @since 4.4
 		 */
+		@Contract("_ -> new")
 		public TimeSeriesOptions expireAfter(Duration ttl) {
 			return new TimeSeriesOptions(timeField, metaField, granularity, ttl);
 		}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionPreparerSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionPreparerSupport.java
index 644a3a54d1..bdf0b90ee3 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionPreparerSupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionPreparerSupport.java
@@ -21,6 +21,7 @@
 import java.util.function.Function;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 
 import com.mongodb.ReadConcern;
 import com.mongodb.ReadPreference;
@@ -84,7 +85,7 @@ public boolean hasReadConcern() {
 	}
 
 	@Override
-	public ReadConcern getReadConcern() {
+	public @Nullable ReadConcern getReadConcern() {
 
 		for (Object aware : sources) {
 			if (aware instanceof ReadConcernAware rca && rca.hasReadConcern()) {
@@ -108,7 +109,7 @@ public boolean hasReadPreference() {
 	}
 
 	@Override
-	public ReadPreference getReadPreference() {
+	public @Nullable ReadPreference getReadPreference() {
 
 		for (Object aware : sources) {
 			if (aware instanceof ReadPreferenceAware rpa && rpa.hasReadPreference()) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CountQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CountQuery.java
index 4fa6b3e97d..11d9f09afd 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CountQuery.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CountQuery.java
@@ -23,9 +23,9 @@
 import java.util.Map;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.geo.Point;
 import org.springframework.data.mongodb.core.query.MetricConversion;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ObjectUtils;
 
 /**
@@ -154,7 +154,7 @@ private Collection<Object> rewriteCollection(Collection<?> source) {
 	 * @param $and potentially existing {@code $and} condition.
 	 * @return the rewritten query {@link Document}.
 	 */
-	@SuppressWarnings("unchecked")
+	@SuppressWarnings({ "unchecked", "NullAway" })
 	private static Document createGeoWithin(String key, Document source, @Nullable Object $and) {
 
 		boolean spheric = source.containsKey("$nearSphere");
@@ -233,6 +233,7 @@ private static boolean containsNearWithMinDistance(Document source) {
 		return source.containsKey("$minDistance");
 	}
 
+	@SuppressWarnings("NullAway")
 	private static Object toCenterCoordinates(Object value) {
 
 		if (ObjectUtils.isArray(value)) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CursorPreparer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CursorPreparer.java
index 9b7408b0cf..3b53cef8d0 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CursorPreparer.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CursorPreparer.java
@@ -18,7 +18,7 @@
 import java.util.function.Function;
 
 import org.bson.Document;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.Assert;
 
 import com.mongodb.ReadPreference;
@@ -76,8 +76,7 @@ default FindIterable<Document> initiateFind(MongoCollection<Document> collection
 	 * @since 2.2
 	 */
 	@Override
-	@Nullable
-	default ReadPreference getReadPreference() {
+	default @Nullable ReadPreference getReadPreference() {
 		return null;
 	}
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DbCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DbCallback.java
index 9d588ad16d..f450bddb30 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DbCallback.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DbCallback.java
@@ -15,8 +15,8 @@
  */
 package org.springframework.data.mongodb.core;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.dao.DataAccessException;
-import org.springframework.lang.Nullable;
 
 import com.mongodb.MongoException;
 import com.mongodb.client.MongoDatabase;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java
index 52343522a7..8bc5349e61 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java
@@ -21,6 +21,7 @@
 import java.util.stream.Collectors;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.context.ApplicationEvent;
 import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.dao.DataIntegrityViolationException;
@@ -40,7 +41,7 @@
 import org.springframework.data.mongodb.core.query.Update;
 import org.springframework.data.mongodb.core.query.UpdateDefinition;
 import org.springframework.data.util.Pair;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 
 import com.mongodb.MongoBulkWriteException;
@@ -115,6 +116,7 @@ void setDefaultWriteConcern(@Nullable WriteConcern defaultWriteConcern) {
 	}
 
 	@Override
+	@Contract("_ -> this")
 	public BulkOperations insert(Object document) {
 
 		Assert.notNull(document, "Document must not be null");
@@ -127,6 +129,7 @@ public BulkOperations insert(Object document) {
 	}
 
 	@Override
+	@Contract("_ -> this")
 	public BulkOperations insert(List<? extends Object> documents) {
 
 		Assert.notNull(documents, "Documents must not be null");
@@ -137,6 +140,7 @@ public BulkOperations insert(List<? extends Object> documents) {
 	}
 
 	@Override
+	@Contract("_, _ -> this")
 	public BulkOperations updateOne(Query query, UpdateDefinition update) {
 
 		Assert.notNull(query, "Query must not be null");
@@ -146,6 +150,7 @@ public BulkOperations updateOne(Query query, UpdateDefinition update) {
 	}
 
 	@Override
+	@Contract("_ -> this")
 	public BulkOperations updateOne(List<Pair<Query, UpdateDefinition>> updates) {
 
 		Assert.notNull(updates, "Updates must not be null");
@@ -158,6 +163,7 @@ public BulkOperations updateOne(List<Pair<Query, UpdateDefinition>> updates) {
 	}
 
 	@Override
+	@Contract("_, _ -> this")
 	public BulkOperations updateMulti(Query query, UpdateDefinition update) {
 
 		Assert.notNull(query, "Query must not be null");
@@ -169,6 +175,7 @@ public BulkOperations updateMulti(Query query, UpdateDefinition update) {
 	}
 
 	@Override
+	@Contract("_ -> this")
 	public BulkOperations updateMulti(List<Pair<Query, UpdateDefinition>> updates) {
 
 		Assert.notNull(updates, "Updates must not be null");
@@ -181,11 +188,13 @@ public BulkOperations updateMulti(List<Pair<Query, UpdateDefinition>> updates) {
 	}
 
 	@Override
+	@Contract("_, _ -> this")
 	public BulkOperations upsert(Query query, UpdateDefinition update) {
 		return update(query, update, true, true);
 	}
 
 	@Override
+	@Contract("_ -> this")
 	public BulkOperations upsert(List<Pair<Query, Update>> updates) {
 
 		for (Pair<Query, Update> update : updates) {
@@ -196,6 +205,7 @@ public BulkOperations upsert(List<Pair<Query, Update>> updates) {
 	}
 
 	@Override
+	@Contract("_ -> this")
 	public BulkOperations remove(Query query) {
 
 		Assert.notNull(query, "Query must not be null");
@@ -209,6 +219,7 @@ public BulkOperations remove(Query query) {
 	}
 
 	@Override
+	@Contract("_ -> this")
 	public BulkOperations remove(List<Query> removes) {
 
 		Assert.notNull(removes, "Removals must not be null");
@@ -221,6 +232,7 @@ public BulkOperations remove(List<Query> removes) {
 	}
 
 	@Override
+	@Contract("_, _, _ -> this")
 	public BulkOperations replaceOne(Query query, Object replacement, FindAndReplaceOptions options) {
 
 		Assert.notNull(query, "Query must not be null");
@@ -412,7 +424,7 @@ public boolean skipEventPublishing() {
 			return eventPublisher == null;
 		}
 
-		@SuppressWarnings("rawtypes")
+		@SuppressWarnings({ "rawtypes", "NullAway" })
 		public <T> T callback(Class<? extends EntityCallback> callbackType, T entity, String collectionName) {
 
 			if (skipEntityCallbacks()) {
@@ -422,7 +434,7 @@ public <T> T callback(Class<? extends EntityCallback> callbackType, T entity, St
 			return entityCallbacks.callback(callbackType, entity, collectionName);
 		}
 
-		@SuppressWarnings("rawtypes")
+		@SuppressWarnings({ "rawtypes", "NullAway" })
 		public <T> T callback(Class<? extends EntityCallback> callbackType, T entity, Document document,
 				String collectionName) {
 
@@ -433,6 +445,7 @@ public <T> T callback(Class<? extends EntityCallback> callbackType, T entity, Do
 			return entityCallbacks.callback(callbackType, entity, document, collectionName);
 		}
 
+		@SuppressWarnings("NullAway")
 		public void publishEvent(ApplicationEvent event) {
 
 			if (skipEventPublishing()) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java
index 2057e2f046..24d22bd80a 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java
@@ -20,6 +20,7 @@
 import java.util.List;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.dao.DataAccessException;
 import org.springframework.data.mongodb.MongoDatabaseFactory;
 import org.springframework.data.mongodb.UncategorizedMongoDbException;
@@ -28,7 +29,6 @@
 import org.springframework.data.mongodb.core.index.IndexInfo;
 import org.springframework.data.mongodb.core.index.IndexOperations;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.NumberUtils;
 
@@ -115,6 +115,7 @@ public DefaultIndexOperations(MongoOperations mongoOperations, String collection
 	}
 
 	@Override
+	@SuppressWarnings("NullAway")
 	public String ensureIndex(IndexDefinition indexDefinition) {
 
 		return execute(collection -> {
@@ -131,8 +132,7 @@ public String ensureIndex(IndexDefinition indexDefinition) {
 		});
 	}
 
-	@Nullable
-	private MongoPersistentEntity<?> lookupPersistentEntity(@Nullable Class<?> entityType, String collection) {
+	private @Nullable MongoPersistentEntity<?> lookupPersistentEntity(@Nullable Class<?> entityType, String collection) {
 
 		if (entityType != null) {
 			return mapper.getMappingContext().getRequiredPersistentEntity(entityType);
@@ -160,6 +160,7 @@ public void dropIndex(String name) {
 	}
 
 	@Override
+	@SuppressWarnings("NullAway")
 	public void alterIndex(String name, org.springframework.data.mongodb.core.index.IndexOptions options) {
 
 		Document indexOptions = new Document("name", name);
@@ -180,6 +181,7 @@ public void dropAllIndexes() {
 	}
 
 	@Override
+	@SuppressWarnings("NullAway")
 	public List<IndexInfo> getIndexInfo() {
 
 		return execute(new CollectionCallback<List<IndexInfo>>() {
@@ -208,8 +210,7 @@ private List<IndexInfo> getIndexData(MongoCursor<Document> cursor) {
 		});
 	}
 
-	@Nullable
-	public <T> T execute(CollectionCallback<T> callback) {
+	public <T> @Nullable T execute(CollectionCallback<T> callback) {
 
 		Assert.notNull(callback, "CollectionCallback must not be null");
 
@@ -228,6 +229,7 @@ private IndexOptions addPartialFilterIfPresent(IndexOptions ops, Document source
 				mapper.getMappedSort((Document) sourceOptions.get(PARTIAL_FILTER_EXPRESSION_KEY), entity));
 	}
 
+	@SuppressWarnings("NullAway")
 	private static IndexOptions addDefaultCollationIfRequired(IndexOptions ops,
 			@Nullable MongoPersistentEntity<?> entity) {
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperationsProvider.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperationsProvider.java
index e2471dbb14..a34c1fb945 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperationsProvider.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperationsProvider.java
@@ -15,6 +15,7 @@
  */
 package org.springframework.data.mongodb.core;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.MongoDatabaseFactory;
 import org.springframework.data.mongodb.core.convert.QueryMapper;
 import org.springframework.data.mongodb.core.index.IndexOperations;
@@ -43,7 +44,7 @@ class DefaultIndexOperationsProvider implements IndexOperationsProvider {
 	}
 
 	@Override
-	public IndexOperations indexOps(String collectionName, Class<?> type) {
+	public IndexOperations indexOps(String collectionName, @Nullable Class<?> type) {
 		return new DefaultIndexOperations(mongoDbFactory, collectionName, mapper, type);
 	}
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveBulkOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveBulkOperations.java
index 59b7ccd63e..92c6a957dc 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveBulkOperations.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveBulkOperations.java
@@ -15,6 +15,7 @@
  */
 package org.springframework.data.mongodb.core;
 
+import org.springframework.lang.Contract;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
@@ -24,6 +25,7 @@
 import java.util.stream.Collectors;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.context.ApplicationEvent;
 import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.data.mapping.callback.EntityCallback;
@@ -40,7 +42,6 @@
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mongodb.core.query.Update;
 import org.springframework.data.mongodb.core.query.UpdateDefinition;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
 import com.mongodb.WriteConcern;
@@ -107,6 +108,7 @@ void setDefaultWriteConcern(@Nullable WriteConcern defaultWriteConcern) {
 	}
 
 	@Override
+	@Contract("_ -> this")
 	public ReactiveBulkOperations insert(Object document) {
 
 		Assert.notNull(document, "Document must not be null");
@@ -120,6 +122,7 @@ public ReactiveBulkOperations insert(Object document) {
 	}
 
 	@Override
+	@Contract("_ -> this")
 	public ReactiveBulkOperations insert(List<? extends Object> documents) {
 
 		Assert.notNull(documents, "Documents must not be null");
@@ -130,6 +133,7 @@ public ReactiveBulkOperations insert(List<? extends Object> documents) {
 	}
 
 	@Override
+	@Contract("_, _, _ -> this")
 	public ReactiveBulkOperations updateOne(Query query, UpdateDefinition update) {
 
 		Assert.notNull(query, "Query must not be null");
@@ -140,6 +144,7 @@ public ReactiveBulkOperations updateOne(Query query, UpdateDefinition update) {
 	}
 
 	@Override
+	@Contract("_, _ -> this")
 	public ReactiveBulkOperations updateMulti(Query query, UpdateDefinition update) {
 
 		Assert.notNull(query, "Query must not be null");
@@ -150,11 +155,13 @@ public ReactiveBulkOperations updateMulti(Query query, UpdateDefinition update)
 	}
 
 	@Override
+	@Contract("_, _ -> this")
 	public ReactiveBulkOperations upsert(Query query, UpdateDefinition update) {
 		return update(query, update, true, true);
 	}
 
 	@Override
+	@Contract("_ -> this")
 	public ReactiveBulkOperations remove(Query query) {
 
 		Assert.notNull(query, "Query must not be null");
@@ -169,6 +176,7 @@ public ReactiveBulkOperations remove(Query query) {
 	}
 
 	@Override
+	@Contract("_ -> this")
 	public ReactiveBulkOperations remove(List<Query> removes) {
 
 		Assert.notNull(removes, "Removals must not be null");
@@ -181,6 +189,7 @@ public ReactiveBulkOperations remove(List<Query> removes) {
 	}
 
 	@Override
+	@Contract("_, _, _ -> this")
 	public ReactiveBulkOperations replaceOne(Query query, Object replacement, FindAndReplaceOptions options) {
 
 		Assert.notNull(query, "Query must not be null");
@@ -359,7 +368,7 @@ public boolean skipEventPublishing() {
 			return eventPublisher == null;
 		}
 
-		@SuppressWarnings("rawtypes")
+		@SuppressWarnings({ "rawtypes", "NullAway" })
 		public <T> Mono<T> callback(Class<? extends EntityCallback> callbackType, T entity, String collectionName) {
 
 			if (skipEntityCallbacks()) {
@@ -369,7 +378,7 @@ public <T> Mono<T> callback(Class<? extends EntityCallback> callbackType, T enti
 			return entityCallbacks.callback(callbackType, entity, collectionName);
 		}
 
-		@SuppressWarnings("rawtypes")
+		@SuppressWarnings({ "rawtypes", "NullAway" })
 		public <T> Mono<T> callback(Class<? extends EntityCallback> callbackType, T entity, Document document,
 				String collectionName) {
 
@@ -380,6 +389,7 @@ public <T> Mono<T> callback(Class<? extends EntityCallback> callbackType, T enti
 			return entityCallbacks.callback(callbackType, entity, document, collectionName);
 		}
 
+		@SuppressWarnings("NullAway")
 		public void publishEvent(ApplicationEvent event) {
 
 			if (skipEventPublishing()) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java
index 8e78f421f4..69ade2e163 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java
@@ -19,16 +19,15 @@
 import reactor.core.publisher.Mono;
 
 import java.util.Collection;
-import java.util.Optional;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.UncategorizedMongoDbException;
 import org.springframework.data.mongodb.core.convert.QueryMapper;
 import org.springframework.data.mongodb.core.index.IndexDefinition;
 import org.springframework.data.mongodb.core.index.IndexInfo;
 import org.springframework.data.mongodb.core.index.ReactiveIndexOperations;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.NumberUtils;
 
@@ -48,7 +47,7 @@ public class DefaultReactiveIndexOperations implements ReactiveIndexOperations {
 	private final ReactiveMongoOperations mongoOperations;
 	private final String collectionName;
 	private final QueryMapper queryMapper;
-	private final Optional<Class<?>> type;
+	private final @Nullable Class<?> type;
 
 	/**
 	 * Creates a new {@link DefaultReactiveIndexOperations}.
@@ -59,7 +58,7 @@ public class DefaultReactiveIndexOperations implements ReactiveIndexOperations {
 	 */
 	public DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations, String collectionName,
 			QueryMapper queryMapper) {
-		this(mongoOperations, collectionName, queryMapper, Optional.empty());
+		this(mongoOperations, collectionName, queryMapper, null);
 	}
 
 	/**
@@ -71,12 +70,7 @@ public DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations, S
 	 * @param type used for mapping potential partial index filter expression, must not be {@literal null}.
 	 */
 	public DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations, String collectionName,
-			QueryMapper queryMapper, Class<?> type) {
-		this(mongoOperations, collectionName, queryMapper, Optional.of(type));
-	}
-
-	private DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations, String collectionName,
-			QueryMapper queryMapper, Optional<Class<?>> type) {
+			QueryMapper queryMapper, @Nullable Class<?> type) {
 
 		Assert.notNull(mongoOperations, "ReactiveMongoOperations must not be null");
 		Assert.notNull(collectionName, "Collection must not be null");
@@ -89,13 +83,12 @@ private DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations,
 	}
 
 	@Override
+	@SuppressWarnings("NullAway")
 	public Mono<String> ensureIndex(IndexDefinition indexDefinition) {
 
 		return mongoOperations.execute(collectionName, collection -> {
 
-			MongoPersistentEntity<?> entity = type
-					.map(val -> (MongoPersistentEntity) queryMapper.getMappingContext().getRequiredPersistentEntity(val))
-					.orElseGet(() -> lookupPersistentEntity(collectionName));
+			MongoPersistentEntity<?> entity = getConfiguredEntity();
 
 			IndexOptions indexOptions = IndexConverters.indexDefinitionToIndexOptionsConverter().convert(indexDefinition);
 
@@ -124,8 +117,7 @@ public Mono<Void> alterIndex(String name, org.springframework.data.mongodb.core.
 		}).then();
 	}
 
-	@Nullable
-	private MongoPersistentEntity<?> lookupPersistentEntity(String collection) {
+	private @Nullable MongoPersistentEntity<?> lookupPersistentEntity(String collection) {
 
 		Collection<? extends MongoPersistentEntity<?>> entities = queryMapper.getMappingContext().getPersistentEntities();
 
@@ -152,6 +144,14 @@ public Flux<IndexInfo> getIndexInfo() {
 				.map(IndexConverters.documentToIndexInfoConverter()::convert);
 	}
 
+	private @Nullable MongoPersistentEntity<?> getConfiguredEntity() {
+
+		if (type != null) {
+			return queryMapper.getMappingContext().getRequiredPersistentEntity(type);
+		}
+		return lookupPersistentEntity(collectionName);
+	}
+
 	private IndexOptions addPartialFilterIfPresent(IndexOptions ops, Document sourceOptions,
 			@Nullable MongoPersistentEntity<?> entity) {
 
@@ -164,6 +164,7 @@ private IndexOptions addPartialFilterIfPresent(IndexOptions ops, Document source
 				queryMapper.getMappedObject((Document) sourceOptions.get(PARTIAL_FILTER_EXPRESSION_KEY), entity));
 	}
 
+	@SuppressWarnings("NullAway")
 	private static IndexOptions addDefaultCollationIfRequired(IndexOptions ops,
 			@Nullable MongoPersistentEntity<?> entity) {
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultScriptOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultScriptOperations.java
index b236b4df28..6dde79e0e8 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultScriptOperations.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultScriptOperations.java
@@ -15,9 +15,9 @@
  */
 package org.springframework.data.mongodb.core;
 
-import static java.util.UUID.*;
-import static org.springframework.data.mongodb.core.query.Criteria.*;
-import static org.springframework.data.mongodb.core.query.Query.*;
+import static java.util.UUID.randomUUID;
+import static org.springframework.data.mongodb.core.query.Criteria.where;
+import static org.springframework.data.mongodb.core.query.Query.query;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -28,7 +28,8 @@
 
 import org.bson.Document;
 import org.bson.types.ObjectId;
-import org.springframework.dao.DataAccessException;
+import org.jspecify.annotations.NullUnmarked;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.mapping.FieldName;
 import org.springframework.data.mongodb.core.script.ExecutableMongoScript;
 import org.springframework.data.mongodb.core.script.NamedMongoScript;
@@ -38,8 +39,6 @@
 import org.springframework.util.StringUtils;
 
 import com.mongodb.BasicDBList;
-import com.mongodb.MongoException;
-import com.mongodb.client.MongoDatabase;
 
 /**
  * Default implementation of {@link ScriptOperations} capable of saving and executing {@link ExecutableMongoScript}.
@@ -51,6 +50,7 @@
  * @deprecated since 2.2. The {@code eval} command has been removed in MongoDB Server 4.2.0.
  */
 @Deprecated
+@NullUnmarked
 class DefaultScriptOperations implements ScriptOperations {
 
 	private static final String SCRIPT_COLLECTION_NAME = "system.js";
@@ -85,38 +85,28 @@ public NamedMongoScript register(NamedMongoScript script) {
 	}
 
 	@Override
-	public Object execute(ExecutableMongoScript script, Object... args) {
+	public @Nullable Object execute(ExecutableMongoScript script, Object... args) {
 
 		Assert.notNull(script, "Script must not be null");
 
-		return mongoOperations.execute(new DbCallback<Object>() {
+		return mongoOperations.execute(db -> {
 
-			@Override
-			public Object doInDB(MongoDatabase db) throws MongoException, DataAccessException {
-
-				Document command = new Document("$eval", script.getCode());
-				BasicDBList commandArgs = new BasicDBList();
-				commandArgs.addAll(Arrays.asList(convertScriptArgs(false, args)));
-				command.append("args", commandArgs);
-				return db.runCommand(command).get("retval");
-			}
+			Document command = new Document("$eval", script.getCode());
+			BasicDBList commandArgs = new BasicDBList();
+			commandArgs.addAll(Arrays.asList(convertScriptArgs(false, args)));
+			command.append("args", commandArgs);
+			return db.runCommand(command).get("retval");
 		});
 	}
 
 	@Override
-	public Object call(String scriptName, Object... args) {
+	public @Nullable Object call(String scriptName, Object... args) {
 
 		Assert.hasText(scriptName, "ScriptName must not be null or empty");
 
-		return mongoOperations.execute(new DbCallback<Object>() {
-
-			@Override
-			public Object doInDB(MongoDatabase db) throws MongoException, DataAccessException {
-
-				return db.runCommand(new Document("eval", String.format("%s(%s)", scriptName, convertAndJoinScriptArgs(args))))
-						.get("retval");
-			}
-		});
+		return mongoOperations.execute(
+				db -> db.runCommand(new Document("eval", String.format("%s(%s)", scriptName, convertAndJoinScriptArgs(args))))
+						.get("retval"));
 	}
 
 	@Override
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultWriteConcernResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultWriteConcernResolver.java
index 8b4de14e05..c445e06f8a 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultWriteConcernResolver.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultWriteConcernResolver.java
@@ -15,6 +15,8 @@
  */
 package org.springframework.data.mongodb.core;
 
+import org.jspecify.annotations.Nullable;
+
 import com.mongodb.WriteConcern;
 
 /**
@@ -26,7 +28,7 @@ enum DefaultWriteConcernResolver implements WriteConcernResolver {
 
 	INSTANCE;
 
-	public WriteConcern resolve(MongoAction action) {
+	public @Nullable WriteConcern resolve(MongoAction action) {
 		return action.getDefaultWriteConcern();
 	}
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EncryptionAlgorithms.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EncryptionAlgorithms.java
index f64391e8cd..601b6898b8 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EncryptionAlgorithms.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EncryptionAlgorithms.java
@@ -19,11 +19,13 @@
  * Encryption algorithms supported by MongoDB Client Side Field Level Encryption.
  *
  * @author Christoph Strobl
+ * @author Ross Lawley
  * @since 3.3
  */
 public final class EncryptionAlgorithms {
 
 	public static final String AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic";
 	public static final String AEAD_AES_256_CBC_HMAC_SHA_512_Random = "AEAD_AES_256_CBC_HMAC_SHA_512-Random";
+	public static final String RANGE = "Range";
 
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityLifecycleEventDelegate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityLifecycleEventDelegate.java
index 94352ad65c..ad3c2b8564 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityLifecycleEventDelegate.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityLifecycleEventDelegate.java
@@ -15,8 +15,8 @@
  */
 package org.springframework.data.mongodb.core;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.context.ApplicationEventPublisher;
-import org.springframework.lang.Nullable;
 
 /**
  * Delegate class to encapsulate lifecycle event configuration and publishing.
@@ -47,6 +47,7 @@ public void setEventsEnabled(boolean eventsEnabled) {
 	 *
 	 * @param event the application event.
 	 */
+	@SuppressWarnings("NullAway")
 	public void publishEvent(Object event) {
 
 		if (canPublishEvent()) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java
index 65a5131dd1..1327656356 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java
@@ -22,12 +22,15 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
 
 import org.bson.BsonNull;
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.core.convert.ConversionService;
 import org.springframework.core.env.Environment;
 import org.springframework.core.env.EnvironmentCapable;
+import org.springframework.core.env.StandardEnvironment;
 import org.springframework.dao.InvalidDataAccessApiUsageException;
 import org.springframework.data.convert.CustomConversions;
 import org.springframework.data.expression.ValueEvaluationContext;
@@ -39,6 +42,7 @@
 import org.springframework.data.mapping.PropertyPath;
 import org.springframework.data.mapping.context.MappingContext;
 import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
+import org.springframework.data.mongodb.core.CollectionOptions.EncryptedFieldsOptions;
 import org.springframework.data.mongodb.core.CollectionOptions.TimeSeriesOptions;
 import org.springframework.data.mongodb.core.convert.MongoConverter;
 import org.springframework.data.mongodb.core.convert.MongoJsonSchemaMapper;
@@ -63,7 +67,6 @@
 import org.springframework.data.projection.TargetAware;
 import org.springframework.data.util.Optionals;
 import org.springframework.expression.spel.support.SimpleEvaluationContext;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.LinkedMultiValueMap;
@@ -83,6 +86,7 @@
  * @author Mark Paluch
  * @author Christoph Strobl
  * @author Ben Foster
+ * @author Ross Lawley
  * @since 2.1
  * @see MongoTemplate
  * @see ReactiveMongoTemplate
@@ -375,8 +379,15 @@ public CreateCollectionOptions convertToCreateCollectionOptions(@Nullable Collec
 			result.timeSeriesOptions(options);
 		});
 
-		collectionOptions.getChangeStreamOptions().ifPresent(it -> result
-				.changeStreamPreAndPostImagesOptions(new ChangeStreamPreAndPostImagesOptions(it.getPreAndPostImages())));
+		collectionOptions.getChangeStreamOptions() //
+				.map(CollectionOptions.CollectionChangeStreamOptions::getPreAndPostImages) //
+				.map(ChangeStreamPreAndPostImagesOptions::new) //
+				.ifPresent(result::changeStreamPreAndPostImagesOptions);
+
+		collectionOptions.getEncryptedFieldsOptions() //
+				.map(EncryptedFieldsOptions::toDocument) //
+				.filter(Predicate.not(Document::isEmpty)) //
+				.ifPresent(result::encryptedFields);
 
 		return result;
 	}
@@ -413,6 +424,7 @@ interface Entity<T> {
 		 *
 		 * @return
 		 */
+		@Nullable
 		Object getId();
 
 		/**
@@ -518,10 +530,9 @@ interface AdaptibleEntity<T> extends Entity<T> {
 		 * Populates the identifier of the backing entity if it has an identifier property and there's no identifier
 		 * currently present.
 		 *
-		 * @param id must not be {@literal null}.
+		 * @param id can be {@literal null}.
 		 * @return
 		 */
-		@Nullable
 		T populateIdIfNecessary(@Nullable Object id);
 
 		/**
@@ -564,12 +575,12 @@ public String getIdFieldName() {
 		}
 
 		@Override
-		public Object getId() {
+		public @Nullable Object getId() {
 			return getPropertyValue(ID_FIELD);
 		}
 
 		@Override
-		public Object getPropertyValue(String key) {
+		public @Nullable Object getPropertyValue(String key) {
 			return map.get(key);
 		}
 
@@ -578,7 +589,6 @@ public Query getByIdQuery() {
 			return Query.query(Criteria.where(ID_FIELD).is(map.get(ID_FIELD)));
 		}
 
-		@Nullable
 		@Override
 		public T populateIdIfNecessary(@Nullable Object id) {
 
@@ -605,8 +615,7 @@ public T initializeVersionProperty() {
 		}
 
 		@Override
-		@Nullable
-		public Number getVersion() {
+		public @Nullable Number getVersion() {
 			return null;
 		}
 
@@ -723,7 +732,7 @@ public Object getId() {
 		}
 
 		@Override
-		public Object getPropertyValue(String key) {
+		public @Nullable Object getPropertyValue(String key) {
 			return propertyAccessor.getProperty(entity.getRequiredPersistentProperty(key));
 		}
 
@@ -790,8 +799,7 @@ public boolean isVersionedEntity() {
 		}
 
 		@Override
-		@Nullable
-		public Object getVersion() {
+		public @Nullable Object getVersion() {
 			return propertyAccessor.getProperty(entity.getRequiredVersionProperty());
 		}
 
@@ -839,7 +847,6 @@ public Map<String, Object> extractKeys(Document sortObject, Class<?> sourceType)
 			return keyset;
 		}
 
-		@Nullable
 		private Object getNestedPropertyValue(String key) {
 
 			String[] segments = key.split("\\.");
@@ -852,6 +859,10 @@ private Object getNestedPropertyValue(String key) {
 				currentValue = currentEntity.getPropertyValue(segment);
 
 				if (i < segments.length - 1) {
+					if (currentValue == null) {
+						return BsonNull.VALUE;
+					}
+
 					currentEntity = entityOperations.forEntity(currentValue);
 				}
 			}
@@ -888,7 +899,6 @@ private static <T> AdaptibleEntity<T> of(T bean,
 					new ConvertingPropertyAccessor<>(propertyAccessor, conversionService), entityOperations);
 		}
 
-		@Nullable
 		@Override
 		public T populateIdIfNecessary(@Nullable Object id) {
 
@@ -910,8 +920,7 @@ public T populateIdIfNecessary(@Nullable Object id) {
 		}
 
 		@Override
-		@Nullable
-		public Number getVersion() {
+		public @Nullable Number getVersion() {
 
 			MongoPersistentProperty versionProperty = entity.getRequiredVersionProperty();
 
@@ -1127,7 +1136,7 @@ public TimeSeriesOptions mapTimeSeriesOptions(TimeSeriesOptions source) {
 
 		@Override
 		public String getIdKeyName() {
-			return entity.getIdProperty().getName();
+			return entity.getIdProperty() != null ? entity.getIdProperty().getName() : ID_FIELD;
 		}
 
 		private String mappedNameOrDefault(String name) {
@@ -1147,7 +1156,8 @@ private ValueEvaluationContext getEvaluationContextForEntity(@Nullable Persisten
 				return mongoEntity.getValueEvaluationContext(null);
 			}
 
-			return ValueEvaluationContext.of(this.environment, SimpleEvaluationContext.forReadOnlyDataBinding().build());
+			return ValueEvaluationContext.of(this.environment != null ? this.environment : new StandardEnvironment(),
+					SimpleEvaluationContext.forReadOnlyDataBinding().build());
 		}
 
 		/**
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/JmxServer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityResultConverter.java
similarity index 52%
rename from spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/JmxServer.java
rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityResultConverter.java
index 004bda1544..c04ae9d603 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/JmxServer.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityResultConverter.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2025 the original author or authors.
+ * 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.
@@ -15,24 +15,19 @@
  */
 package org.springframework.data.mongodb.core;
 
-import org.springframework.context.support.ClassPathXmlApplicationContext;
+import org.bson.Document;
 
-/**
- * Server application than can be run as an app or unit test.
- *
- * @author Mark Pollack
- * @author Oliver Gierke
- * @deprecated since 4.5.
- */
-@Deprecated(since = "4.5", forRemoval = true)
-public class JmxServer {
+enum EntityResultConverter implements QueryResultConverter<Object, Object> {
+
+	INSTANCE;
 
-	public static void main(String[] args) {
-		new JmxServer().run();
+	@Override
+	public Object mapDocument(Document document, ConversionResultSupplier<Object> reader) {
+		return reader.get();
 	}
 
-	@SuppressWarnings("resource")
-	public void run() {
-		new ClassPathXmlApplicationContext(new String[] { "infrastructure.xml", "server-jmx.xml" });
+	@Override
+	public <V> QueryResultConverter<Object, V> andThen(QueryResultConverter<? super Object, ? extends V> after) {
+		return (QueryResultConverter) after;
 	}
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperation.java
index 67ed188655..57813a75ba 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperation.java
@@ -19,6 +19,7 @@
 
 import org.springframework.data.mongodb.core.aggregation.Aggregation;
 import org.springframework.data.mongodb.core.aggregation.AggregationResults;
+import org.springframework.lang.Contract;
 
 /**
  * {@link ExecutableAggregationOperation} allows creation and execution of MongoDB aggregation operations in a fluent
@@ -45,7 +46,7 @@ public interface ExecutableAggregationOperation {
 	/**
 	 * Start creating an aggregation operation that returns results mapped to the given domain type. <br />
 	 * Use {@link org.springframework.data.mongodb.core.aggregation.TypedAggregation} to specify a potentially different
-	 * input type for he aggregation.
+	 * input type for the aggregation.
 	 *
 	 * @param domainType must not be {@literal null}.
 	 * @return new instance of {@link ExecutableAggregation}.
@@ -76,10 +77,23 @@ interface AggregationWithCollection<T> {
 	 * Trigger execution by calling one of the terminating methods.
 	 *
 	 * @author Christoph Strobl
+	 * @author Mark Paluch
 	 * @since 2.0
 	 */
 	interface TerminatingAggregation<T> {
 
+		/**
+		 * Map the query result to a different type using {@link QueryResultConverter}.
+		 *
+		 * @param <R> {@link Class type} of the result.
+		 * @param converter the converter, must not be {@literal null}.
+		 * @return new instance of {@link TerminatingAggregation}.
+		 * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}.
+		 * @since 5.0
+		 */
+		@Contract("_ -> new")
+		<R> TerminatingAggregation<R> map(QueryResultConverter<? super T, ? extends R> converter);
+
 		/**
 		 * Apply pipeline operations as specified and get all matching elements.
 		 *
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupport.java
index ca5aa7a513..13dc8cd436 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupport.java
@@ -17,6 +17,7 @@
 
 import java.util.stream.Stream;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.Aggregation;
 import org.springframework.data.mongodb.core.aggregation.AggregationResults;
 import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
@@ -43,25 +44,28 @@ public <T> ExecutableAggregation<T> aggregateAndReturn(Class<T> domainType) {
 
 		Assert.notNull(domainType, "DomainType must not be null");
 
-		return new ExecutableAggregationSupport<>(template, domainType, null, null);
+		return new ExecutableAggregationSupport<>(template, domainType, QueryResultConverter.entity(), null, null);
 	}
 
 	/**
 	 * @author Christoph Strobl
 	 * @since 2.0
 	 */
-	static class ExecutableAggregationSupport<T>
+	static class ExecutableAggregationSupport<S, T>
 			implements AggregationWithAggregation<T>, ExecutableAggregation<T>, TerminatingAggregation<T> {
 
 		private final MongoTemplate template;
-		private final Class<T> domainType;
-		private final Aggregation aggregation;
-		private final String collection;
-
-		public ExecutableAggregationSupport(MongoTemplate template, Class<T> domainType, Aggregation aggregation,
-				String collection) {
+		private final Class<S> domainType;
+		private final QueryResultConverter<? super S, ? extends T> resultConverter;
+		private final @Nullable Aggregation aggregation;
+		private final @Nullable String collection;
+
+		public ExecutableAggregationSupport(MongoTemplate template, Class<S> domainType,
+				QueryResultConverter<? super S, ? extends T> resultConverter, @Nullable Aggregation aggregation,
+				@Nullable String collection) {
 			this.template = template;
 			this.domainType = domainType;
+			this.resultConverter = resultConverter;
 			this.aggregation = aggregation;
 			this.collection = collection;
 		}
@@ -71,7 +75,7 @@ public AggregationWithAggregation<T> inCollection(String collection) {
 
 			Assert.hasText(collection, "Collection must not be null nor empty");
 
-			return new ExecutableAggregationSupport<>(template, domainType, aggregation, collection);
+			return new ExecutableAggregationSupport<>(template, domainType, resultConverter, aggregation, collection);
 		}
 
 		@Override
@@ -79,26 +83,39 @@ public TerminatingAggregation<T> by(Aggregation aggregation) {
 
 			Assert.notNull(aggregation, "Aggregation must not be null");
 
-			return new ExecutableAggregationSupport<>(template, domainType, aggregation, collection);
+			return new ExecutableAggregationSupport<>(template, domainType, resultConverter, aggregation, collection);
+		}
+
+		@Override
+		public <R> TerminatingAggregation<R> map(QueryResultConverter<? super T, ? extends R> converter) {
+
+			Assert.notNull(converter, "QueryResultConverter must not be null");
+
+			return new ExecutableAggregationSupport<>(template, domainType, this.resultConverter.andThen(converter),
+					aggregation, collection);
 		}
 
 		@Override
 		public AggregationResults<T> all() {
-			return template.aggregate(aggregation, getCollectionName(aggregation), domainType);
+
+			Assert.notNull(aggregation, "Aggregation must be set first");
+			return template.doAggregate(aggregation, getCollectionName(aggregation), domainType, resultConverter);
 		}
 
 		@Override
 		public Stream<T> stream() {
-			return template.aggregateStream(aggregation, getCollectionName(aggregation), domainType);
+
+			Assert.notNull(aggregation, "Aggregation must be set first");
+			return template.doAggregateStream(aggregation, getCollectionName(aggregation), domainType, resultConverter, null);
 		}
 
-		private String getCollectionName(Aggregation aggregation) {
+		private String getCollectionName(@Nullable Aggregation aggregation) {
 
 			if (StringUtils.hasText(collection)) {
 				return collection;
 			}
 
-			if (aggregation instanceof TypedAggregation typedAggregation) {
+			if (aggregation instanceof TypedAggregation<?> typedAggregation) {
 
 				if (typedAggregation.getInputType() != null) {
 					return template.getCollectionName(typedAggregation.getInputType());
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java
index 3358ff2b17..43c0d521c3 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java
@@ -19,6 +19,7 @@
 import java.util.Optional;
 import java.util.stream.Stream;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.dao.DataAccessException;
 import org.springframework.data.domain.KeysetScrollPosition;
 import org.springframework.data.domain.ScrollPosition;
@@ -27,7 +28,7 @@
 import org.springframework.data.mongodb.core.query.CriteriaDefinition;
 import org.springframework.data.mongodb.core.query.NearQuery;
 import org.springframework.data.mongodb.core.query.Query;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 
 import com.mongodb.client.MongoCollection;
 
@@ -71,9 +72,33 @@ public interface ExecutableFindOperation {
 	 * Trigger find execution by calling one of the terminating methods.
 	 *
 	 * @author Christoph Strobl
+	 * @author Mark Paluch
 	 * @since 2.0
 	 */
-	interface TerminatingFind<T> {
+	interface TerminatingFind<T> extends TerminatingResults<T>, TerminatingProjection {
+
+	}
+
+	/**
+	 * Trigger find execution by calling one of the terminating methods.
+	 *
+	 * @author Christoph Strobl
+	 * @author Mark Paluch
+	 * @since 5.0
+	 */
+	interface TerminatingResults<T> {
+
+		/**
+		 * Map the query result to a different type using {@link QueryResultConverter}.
+		 *
+		 * @param <R> {@link Class type} of the result.
+		 * @param converter the converter, must not be {@literal null}.
+		 * @return new instance of {@link TerminatingResults}.
+		 * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}.
+		 * @since 5.0
+		 */
+		@Contract("_ -> new")
+		<R> TerminatingResults<R> map(QueryResultConverter<? super T, ? extends R> converter);
 
 		/**
 		 * Get exactly zero or one result.
@@ -132,7 +157,7 @@ default Optional<T> first() {
 		 * <p>
 		 * When using {@link KeysetScrollPosition}, make sure to use non-nullable
 		 * {@link org.springframework.data.domain.Sort sort properties} as MongoDB does not support criteria to reconstruct
-		 * a query result from absent document fields or {@code null} values through {@code $gt/$lt} operators.
+		 * a query result from absent document fields or {@literal null} values through {@code $gt/$lt} operators.
 		 *
 		 * @param scrollPosition the scroll position.
 		 * @return a window of the resulting elements.
@@ -142,6 +167,16 @@ default Optional<T> first() {
 		 */
 		Window<T> scroll(ScrollPosition scrollPosition);
 
+	}
+
+	/**
+	 * Trigger find execution by calling one of the terminating methods.
+	 *
+	 * @author Christoph Strobl
+	 * @since 5.0
+	 */
+	interface TerminatingProjection {
+
 		/**
 		 * Get the number of matching elements. <br />
 		 * This method uses an
@@ -160,16 +195,30 @@ default Optional<T> first() {
 		 * @return {@literal true} if at least one matching element exists.
 		 */
 		boolean exists();
+
 	}
 
 	/**
-	 * Trigger geonear execution by calling one of the terminating methods.
+	 * Trigger {@code geoNear} execution by calling one of the terminating methods.
 	 *
 	 * @author Christoph Strobl
+	 * @author Mark Paluch
 	 * @since 2.0
 	 */
 	interface TerminatingFindNear<T> {
 
+		/**
+		 * Map the query result to a different type using {@link QueryResultConverter}.
+		 *
+		 * @param <R> {@link Class type} of the result.
+		 * @param converter the converter, must not be {@literal null}.
+		 * @return new instance of {@link TerminatingFindNear}.
+		 * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}.
+		 * @since 5.0
+		 */
+		@Contract("_ -> new")
+		<R> TerminatingFindNear<R> map(QueryResultConverter<? super T, ? extends R> converter);
+
 		/**
 		 * Find all matching elements and return them as {@link org.springframework.data.geo.GeoResult}.
 		 *
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java
index 4e6c3547c5..46289ecfa4 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java
@@ -16,18 +16,18 @@
 package org.springframework.data.mongodb.core;
 
 import java.util.List;
-import java.util.Optional;
 import java.util.stream.Stream;
 
 import org.bson.Document;
-
+import org.jspecify.annotations.Nullable;
 import org.springframework.dao.IncorrectResultSizeDataAccessException;
 import org.springframework.data.domain.ScrollPosition;
 import org.springframework.data.domain.Window;
+import org.springframework.data.geo.GeoResults;
 import org.springframework.data.mongodb.core.query.NearQuery;
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mongodb.core.query.SerializationUtils;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
@@ -53,11 +53,13 @@ class ExecutableFindOperationSupport implements ExecutableFindOperation {
 	}
 
 	@Override
+	@Contract("_ -> new")
 	public <T> ExecutableFind<T> query(Class<T> domainType) {
 
 		Assert.notNull(domainType, "DomainType must not be null");
 
-		return new ExecutableFindSupport<>(template, domainType, domainType, null, ALL_QUERY);
+		return new ExecutableFindSupport<>(template, domainType, domainType, QueryResultConverter.entity(), null,
+				ALL_QUERY);
 	}
 
 	/**
@@ -65,50 +67,66 @@ public <T> ExecutableFind<T> query(Class<T> domainType) {
 	 * @author Christoph Strobl
 	 * @since 2.0
 	 */
-	static class ExecutableFindSupport<T>
+	static class ExecutableFindSupport<S, T>
 			implements ExecutableFind<T>, FindWithCollection<T>, FindWithProjection<T>, FindWithQuery<T> {
 
 		private final MongoTemplate template;
 		private final Class<?> domainType;
-		private final Class<T> returnType;
+		private final Class<S> returnType;
+		private final QueryResultConverter<? super S, ? extends T> resultConverter;
 		private final @Nullable String collection;
 		private final Query query;
 
-		ExecutableFindSupport(MongoTemplate template, Class<?> domainType, Class<T> returnType, @Nullable String collection,
+		ExecutableFindSupport(MongoTemplate template, Class<?> domainType, Class<S> returnType,
+				QueryResultConverter<? super S, ? extends T> resultConverter, @Nullable String collection,
 				Query query) {
 			this.template = template;
 			this.domainType = domainType;
+			this.resultConverter = resultConverter;
 			this.returnType = returnType;
 			this.collection = collection;
 			this.query = query;
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public FindWithProjection<T> inCollection(String collection) {
 
 			Assert.hasText(collection, "Collection name must not be null nor empty");
 
-			return new ExecutableFindSupport<>(template, domainType, returnType, collection, query);
+			return new ExecutableFindSupport<>(template, domainType, returnType, resultConverter, collection, query);
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public <T1> FindWithQuery<T1> as(Class<T1> returnType) {
 
 			Assert.notNull(returnType, "ReturnType must not be null");
 
-			return new ExecutableFindSupport<>(template, domainType, returnType, collection, query);
+			return new ExecutableFindSupport<>(template, domainType, returnType, QueryResultConverter.entity(), collection,
+					query);
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public TerminatingFind<T> matching(Query query) {
 
 			Assert.notNull(query, "Query must not be null");
 
-			return new ExecutableFindSupport<>(template, domainType, returnType, collection, query);
+			return new ExecutableFindSupport<>(template, domainType, returnType, resultConverter, collection, query);
+		}
+
+		@Override
+		public <R> TerminatingResults<R> map(QueryResultConverter<? super T, ? extends R> converter) {
+
+			Assert.notNull(converter, "QueryResultConverter must not be null");
+
+			return new ExecutableFindSupport<>(template, domainType, returnType, this.resultConverter.andThen(converter),
+					collection, query);
 		}
 
 		@Override
-		public T oneValue() {
+		public @Nullable T oneValue() {
 
 			List<T> result = doFind(new DelegatingQueryCursorPreparer(getCursorPreparer(query, null)).limit(2));
 
@@ -124,7 +142,7 @@ public T oneValue() {
 		}
 
 		@Override
-		public T firstValue() {
+		public @Nullable T firstValue() {
 
 			List<T> result = doFind(new DelegatingQueryCursorPreparer(getCursorPreparer(query, null)).limit(1));
 
@@ -143,12 +161,13 @@ public Stream<T> stream() {
 
 		@Override
 		public Window<T> scroll(ScrollPosition scrollPosition) {
-			return template.doScroll(query.with(scrollPosition), domainType, returnType, getCollectionName());
+			return template.doScroll(query.with(scrollPosition), domainType, returnType, resultConverter,
+					getCollectionName());
 		}
 
 		@Override
 		public TerminatingFindNear<T> near(NearQuery nearQuery) {
-			return () -> template.geoNear(nearQuery, domainType, getCollectionName(), returnType);
+			return new TerminatingFindNearSupport<>(nearQuery, this.resultConverter);
 		}
 
 		@Override
@@ -176,17 +195,17 @@ private List<T> doFind(@Nullable CursorPreparer preparer) {
 			Document fieldsObject = query.getFieldsObject();
 
 			return template.doFind(template.createDelegate(query), getCollectionName(), queryObject, fieldsObject, domainType,
-					returnType, getCursorPreparer(query, preparer));
+					returnType, resultConverter, getCursorPreparer(query, preparer));
 		}
 
 		private List<T> doFindDistinct(String field) {
 
 			return template.findDistinct(query, field, getCollectionName(), domainType,
-					returnType == domainType ? (Class<T>) Object.class : returnType);
+					returnType == domainType ? (Class) Object.class : returnType);
 		}
 
 		private Stream<T> doStream() {
-			return template.doStream(query, domainType, getCollectionName(), returnType);
+			return template.doStream(query, domainType, getCollectionName(), returnType, resultConverter);
 		}
 
 		private CursorPreparer getCursorPreparer(Query query, @Nullable CursorPreparer preparer) {
@@ -200,16 +219,41 @@ private String getCollectionName() {
 		private String asString() {
 			return SerializationUtils.serializeToJsonSafely(query);
 		}
+
+		class TerminatingFindNearSupport<G> implements TerminatingFindNear<G> {
+
+			private final NearQuery nearQuery;
+			private final QueryResultConverter<? super S, ? extends G> resultConverter;
+
+			public TerminatingFindNearSupport(NearQuery nearQuery,
+					QueryResultConverter<? super S, ? extends G> resultConverter) {
+				this.nearQuery = nearQuery;
+				this.resultConverter = resultConverter;
+			}
+
+			@Override
+			public <R> TerminatingFindNear<R> map(QueryResultConverter<? super G, ? extends R> converter) {
+
+				Assert.notNull(converter, "QueryResultConverter must not be null");
+
+				return new TerminatingFindNearSupport<>(nearQuery, this.resultConverter.andThen(converter));
+			}
+
+			@Override
+			public GeoResults<G> all() {
+				return template.doGeoNear(nearQuery, domainType, getCollectionName(), returnType, resultConverter);
+			}
+		}
 	}
 
 	/**
 	 * @author Christoph Strobl
 	 * @since 2.0
 	 */
-	static class DelegatingQueryCursorPreparer implements SortingQueryCursorPreparer {
+	static class DelegatingQueryCursorPreparer implements CursorPreparer {
 
 		private final @Nullable CursorPreparer delegate;
-		private Optional<Integer> limit = Optional.empty();
+		private int limit = -1;
 
 		DelegatingQueryCursorPreparer(@Nullable CursorPreparer delegate) {
 			this.delegate = delegate;
@@ -219,25 +263,22 @@ static class DelegatingQueryCursorPreparer implements SortingQueryCursorPreparer
 		public FindIterable<Document> prepare(FindIterable<Document> iterable) {
 
 			FindIterable<Document> target = delegate != null ? delegate.prepare(iterable) : iterable;
-			return limit.map(target::limit).orElse(target);
+			if (limit >= 0) {
+				target.limit(limit);
+			}
+			return target;
 		}
 
+		@Contract("_ -> this")
 		CursorPreparer limit(int limit) {
 
-			this.limit = Optional.of(limit);
+			this.limit = limit;
 			return this;
 		}
 
 		@Override
-		@Nullable
-		public ReadPreference getReadPreference() {
-			return delegate.getReadPreference();
-		}
-
-		@Override
-		@Nullable
-		public Document getSortObject() {
-			return delegate instanceof SortingQueryCursorPreparer sqcp ? sqcp.getSortObject() : null;
+		public @Nullable ReadPreference getReadPreference() {
+			return delegate != null ? delegate.getReadPreference() : null;
 		}
 	}
 
@@ -245,19 +286,20 @@ public Document getSortObject() {
 	 * @author Christoph Strobl
 	 * @since 2.1
 	 */
-	static class DistinctOperationSupport<T> implements TerminatingDistinct<T> {
+	static class DistinctOperationSupport<S, T> implements TerminatingDistinct<T> {
 
 		private final String field;
-		private final ExecutableFindSupport<T> delegate;
+		private final ExecutableFindSupport<S, T> delegate;
 
-		public DistinctOperationSupport(ExecutableFindSupport<T> delegate, String field) {
+		public DistinctOperationSupport(ExecutableFindSupport<S, T> delegate, String field) {
 
 			this.delegate = delegate;
 			this.field = field;
 		}
 
 		@Override
-		@SuppressWarnings("unchecked")
+		@SuppressWarnings({ "unchecked", "rawtypes" })
+		@Contract("_ -> new")
 		public <R> TerminatingDistinct<R> as(Class<R> resultType) {
 
 			Assert.notNull(resultType, "ResultType must not be null");
@@ -266,16 +308,18 @@ public <R> TerminatingDistinct<R> as(Class<R> resultType) {
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public TerminatingDistinct<T> matching(Query query) {
 
 			Assert.notNull(query, "Query must not be null");
 
-			return new DistinctOperationSupport<>((ExecutableFindSupport<T>) delegate.matching(query), field);
+			return new DistinctOperationSupport<>((ExecutableFindSupport<S, T>) delegate.matching(query), field);
 		}
 
 		@Override
 		public List<T> all() {
 			return delegate.doFindDistinct(field);
 		}
+
 	}
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableInsertOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableInsertOperationSupport.java
index 47b7127deb..599a910035 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableInsertOperationSupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableInsertOperationSupport.java
@@ -18,8 +18,9 @@
 import java.util.ArrayList;
 import java.util.Collection;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.BulkOperations.BulkMode;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 
@@ -41,6 +42,7 @@ class ExecutableInsertOperationSupport implements ExecutableInsertOperation {
 	}
 
 	@Override
+	@Contract("_ -> new")
 	public <T> ExecutableInsert<T> insert(Class<T> domainType) {
 
 		Assert.notNull(domainType, "DomainType must not be null");
@@ -56,10 +58,11 @@ static class ExecutableInsertSupport<T> implements ExecutableInsert<T> {
 
 		private final MongoTemplate template;
 		private final Class<T> domainType;
-		@Nullable private final String collection;
-		@Nullable private final BulkMode bulkMode;
+		private final @Nullable String collection;
+		private final @Nullable BulkMode bulkMode;
 
-		ExecutableInsertSupport(MongoTemplate template, Class<T> domainType, String collection, BulkMode bulkMode) {
+		ExecutableInsertSupport(MongoTemplate template, Class<T> domainType, @Nullable String collection,
+				@Nullable BulkMode bulkMode) {
 
 			this.template = template;
 			this.domainType = domainType;
@@ -93,6 +96,7 @@ public BulkWriteResult bulk(Collection<? extends T> objects) {
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public InsertWithBulkMode<T> inCollection(String collection) {
 
 			Assert.hasText(collection, "Collection must not be null nor empty");
@@ -101,6 +105,7 @@ public InsertWithBulkMode<T> inCollection(String collection) {
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public TerminatingBulkInsert<T> withBulkMode(BulkMode bulkMode) {
 
 			Assert.notNull(bulkMode, "BulkMode must not be null");
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupport.java
index 9f78693540..55864cbd8e 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupport.java
@@ -17,9 +17,10 @@
 
 import java.util.List;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
 import org.springframework.data.mongodb.core.query.Query;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 
@@ -46,6 +47,7 @@ class ExecutableMapReduceOperationSupport implements ExecutableMapReduceOperatio
 	 * @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation#mapReduce(java.lang.Class)
 	 */
 	@Override
+	@Contract("_ -> new")
 	public <T> ExecutableMapReduceSupport<T> mapReduce(Class<T> domainType) {
 
 		Assert.notNull(domainType, "DomainType must not be null");
@@ -89,6 +91,7 @@ static class ExecutableMapReduceSupport<T>
 		 * @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.TerminatingMapReduce#all()
 		 */
 		@Override
+		@SuppressWarnings("NullAway")
 		public List<T> all() {
 			return template.mapReduce(query, domainType, getCollectionName(), mapFunction, reduceFunction, options,
 					returnType);
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java
index a10cd0317f..c29a448f1c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java
@@ -19,6 +19,7 @@
 
 import org.springframework.data.mongodb.core.query.CriteriaDefinition;
 import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.lang.Contract;
 
 import com.mongodb.client.result.DeleteResult;
 
@@ -54,11 +55,40 @@ public interface ExecutableRemoveOperation {
 	 */
 	<T> ExecutableRemove<T> remove(Class<T> domainType);
 
+	/**
+	 * @author Christoph Strobl
+	 * @since 5.0
+	 */
+	interface TerminatingResults<T> {
+
+		/**
+		 * Map the query result to a different type using {@link QueryResultConverter}.
+		 *
+		 * @param <R> {@link Class type} of the result.
+		 * @param converter the converter, must not be {@literal null}.
+		 * @return new instance of {@link ExecutableFindOperation.TerminatingResults}.
+		 * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}.
+		 * @since 5.0
+		 */
+		@Contract("_ -> new")
+		<R> TerminatingResults<R> map(QueryResultConverter<? super T, ? extends R> converter);
+
+		/**
+		 * Remove and return all matching documents. <br/>
+		 * <strong>NOTE:</strong> The entire list of documents will be fetched before sending the actual delete commands.
+		 * Also, {@link org.springframework.context.ApplicationEvent}s will be published for each and every delete
+		 * operation.
+		 *
+		 * @return empty {@link List} if no match found. Never {@literal null}.
+		 */
+		List<T> findAndRemove();
+	}
+
 	/**
 	 * @author Christoph Strobl
 	 * @since 2.0
 	 */
-	interface TerminatingRemove<T> {
+	interface TerminatingRemove<T> extends TerminatingResults<T> {
 
 		/**
 		 * Remove all documents matching.
@@ -73,16 +103,6 @@ interface TerminatingRemove<T> {
 		 * @return the {@link DeleteResult}. Never {@literal null}.
 		 */
 		DeleteResult one();
-
-		/**
-		 * Remove and return all matching documents. <br/>
-		 * <strong>NOTE:</strong> The entire list of documents will be fetched before sending the actual delete commands.
-		 * Also, {@link org.springframework.context.ApplicationEvent}s will be published for each and every delete
-		 * operation.
-		 *
-		 * @return empty {@link List} if no match found. Never {@literal null}.
-		 */
-		List<T> findAndRemove();
 	}
 
 	/**
@@ -105,7 +125,6 @@ interface RemoveWithCollection<T> extends RemoveWithQuery<T> {
 		RemoveWithQuery<T> inCollection(String collection);
 	}
 
-
 	/**
 	 * @author Christoph Strobl
 	 * @since 2.0
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java
index 8e84aa7dd6..7817a7c8af 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java
@@ -17,8 +17,9 @@
 
 import java.util.List;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.query.Query;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 
@@ -42,45 +43,51 @@ public ExecutableRemoveOperationSupport(MongoTemplate tempate) {
 	}
 
 	@Override
+	@Contract("_ -> new")
 	public <T> ExecutableRemove<T> remove(Class<T> domainType) {
 
 		Assert.notNull(domainType, "DomainType must not be null");
 
-		return new ExecutableRemoveSupport<>(tempate, domainType, ALL_QUERY, null);
+		return new ExecutableRemoveSupport<>(tempate, domainType, ALL_QUERY, null, QueryResultConverter.entity());
 	}
 
 	/**
 	 * @author Christoph Strobl
 	 * @since 2.0
 	 */
-	static class ExecutableRemoveSupport<T> implements ExecutableRemove<T>, RemoveWithCollection<T> {
+	static class ExecutableRemoveSupport<S, T> implements ExecutableRemove<T>, RemoveWithCollection<T> {
 
 		private final MongoTemplate template;
-		private final Class<T> domainType;
+		private final Class<S> domainType;
 		private final Query query;
 		@Nullable private final String collection;
+		private final QueryResultConverter<? super S, ? extends T> resultConverter;
 
-		public ExecutableRemoveSupport(MongoTemplate template, Class<T> domainType, Query query, String collection) {
+		public ExecutableRemoveSupport(MongoTemplate template, Class<S> domainType, Query query,
+				@Nullable String collection, QueryResultConverter<? super S, ? extends T> resultConverter) {
 			this.template = template;
 			this.domainType = domainType;
 			this.query = query;
 			this.collection = collection;
+			this.resultConverter = resultConverter;
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public RemoveWithQuery<T> inCollection(String collection) {
 
 			Assert.hasText(collection, "Collection must not be null nor empty");
 
-			return new ExecutableRemoveSupport<>(template, domainType, query, collection);
+			return new ExecutableRemoveSupport<>(template, domainType, query, collection, resultConverter);
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public TerminatingRemove<T> matching(Query query) {
 
 			Assert.notNull(query, "Query must not be null");
 
-			return new ExecutableRemoveSupport<>(template, domainType, query, collection);
+			return new ExecutableRemoveSupport<>(template, domainType, query, collection, resultConverter);
 		}
 
 		@Override
@@ -98,7 +105,13 @@ public List<T> findAndRemove() {
 
 			String collectionName = getCollectionName();
 
-			return template.doFindAndDelete(collectionName, query, domainType);
+			return template.doFindAndDelete(collectionName, query, domainType, resultConverter);
+		}
+
+		@Override
+		@SuppressWarnings({"unchecked", "rawtypes"})
+		public <R> TerminatingResults<R> map(QueryResultConverter<? super T, ? extends R> converter) {
+			return new ExecutableRemoveSupport<>(template, (Class) domainType, query, collection, converter);
 		}
 
 		private String getCollectionName() {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java
index a5c63e9b67..e671b7b7ce 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java
@@ -17,12 +17,14 @@
 
 import java.util.Optional;
 
+import org.jspecify.annotations.Nullable;
+
 import org.springframework.data.mongodb.core.aggregation.AggregationUpdate;
 import org.springframework.data.mongodb.core.query.CriteriaDefinition;
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mongodb.core.query.Update;
 import org.springframework.data.mongodb.core.query.UpdateDefinition;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 
 import com.mongodb.client.result.UpdateResult;
 
@@ -69,6 +71,18 @@ public interface ExecutableUpdateOperation {
 	 */
 	interface TerminatingFindAndModify<T> {
 
+		/**
+		 * Map the query result to a different type using {@link QueryResultConverter}.
+		 *
+		 * @param <R> {@link Class type} of the result.
+		 * @param converter the converter, must not be {@literal null}.
+		 * @return new instance of {@link TerminatingFindAndModify}.
+		 * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}.
+		 * @since 5.0
+		 */
+		@Contract("_ -> new")
+		<R> TerminatingFindAndModify<R> map(QueryResultConverter<? super T, ? extends R> converter);
+
 		/**
 		 * Find, modify and return the first matching document.
 		 *
@@ -130,6 +144,19 @@ default Optional<T> findAndReplace() {
 		 */
 		@Nullable
 		T findAndReplaceValue();
+
+		/**
+		 * Map the query result to a different type using {@link QueryResultConverter}.
+		 *
+		 * @param <R> {@link Class type} of the result.
+		 * @param converter the converter, must not be {@literal null}.
+		 * @return new instance of {@link TerminatingFindAndModify}.
+		 * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}.
+		 * @since 5.0
+		 */
+		@Contract("_ -> new")
+		<R> TerminatingFindAndReplace<R> map(QueryResultConverter<? super T, ? extends R> converter);
+
 	}
 
 	/**
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java
index 593d863d39..dc9ce5cacc 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java
@@ -15,9 +15,10 @@
  */
 package org.springframework.data.mongodb.core;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mongodb.core.query.UpdateDefinition;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 
@@ -41,34 +42,38 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
 	}
 
 	@Override
+	@Contract("_ -> new")
 	public <T> ExecutableUpdate<T> update(Class<T> domainType) {
 
 		Assert.notNull(domainType, "DomainType must not be null");
 
-		return new ExecutableUpdateSupport<>(template, domainType, ALL_QUERY, null, null, null, null, null, domainType);
+		return new ExecutableUpdateSupport<>(template, domainType, ALL_QUERY, null, null, null, null, null, domainType, QueryResultConverter.entity());
 	}
 
 	/**
 	 * @author Christoph Strobl
 	 * @since 2.0
 	 */
-	static class ExecutableUpdateSupport<T>
+	@SuppressWarnings("rawtypes")
+	static class ExecutableUpdateSupport<S, T>
 			implements ExecutableUpdate<T>, UpdateWithCollection<T>, UpdateWithQuery<T>, TerminatingUpdate<T>,
 			FindAndReplaceWithOptions<T>, TerminatingFindAndReplace<T>, FindAndReplaceWithProjection<T> {
 
 		private final MongoTemplate template;
-		private final Class domainType;
+		private final Class<?> domainType;
 		private final Query query;
 		@Nullable private final UpdateDefinition update;
 		@Nullable private final String collection;
 		@Nullable private final FindAndModifyOptions findAndModifyOptions;
 		@Nullable private final FindAndReplaceOptions findAndReplaceOptions;
 		@Nullable private final Object replacement;
-		private final Class<T> targetType;
+		private final QueryResultConverter<? super S, ? extends T> resultConverter;
+		private final Class<S> targetType;
 
-		ExecutableUpdateSupport(MongoTemplate template, Class domainType, Query query, UpdateDefinition update,
-				String collection, FindAndModifyOptions findAndModifyOptions, FindAndReplaceOptions findAndReplaceOptions,
-				Object replacement, Class<T> targetType) {
+		ExecutableUpdateSupport(MongoTemplate template, Class<?> domainType, Query query, @Nullable UpdateDefinition update,
+				@Nullable String collection, @Nullable FindAndModifyOptions findAndModifyOptions,
+				@Nullable FindAndReplaceOptions findAndReplaceOptions, @Nullable Object replacement, Class<S> targetType,
+			QueryResultConverter<? super S, ? extends T> resultConverter) {
 
 			this.template = template;
 			this.domainType = domainType;
@@ -79,54 +84,61 @@ static class ExecutableUpdateSupport<T>
 			this.findAndReplaceOptions = findAndReplaceOptions;
 			this.replacement = replacement;
 			this.targetType = targetType;
+			this.resultConverter = resultConverter;
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public TerminatingUpdate<T> apply(UpdateDefinition update) {
 
 			Assert.notNull(update, "Update must not be null");
 
 			return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
-					findAndReplaceOptions, replacement, targetType);
+					findAndReplaceOptions, replacement, targetType, resultConverter);
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public UpdateWithQuery<T> inCollection(String collection) {
 
 			Assert.hasText(collection, "Collection must not be null nor empty");
 
 			return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
-					findAndReplaceOptions, replacement, targetType);
+					findAndReplaceOptions, replacement, targetType, resultConverter);
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public TerminatingFindAndModify<T> withOptions(FindAndModifyOptions options) {
 
 			Assert.notNull(options, "Options must not be null");
 
 			return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, options,
-					findAndReplaceOptions, replacement, targetType);
+					findAndReplaceOptions, replacement, targetType, resultConverter);
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public FindAndReplaceWithProjection<T> replaceWith(T replacement) {
 
 			Assert.notNull(replacement, "Replacement must not be null");
 
 			return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
-					findAndReplaceOptions, replacement, targetType);
+					findAndReplaceOptions, replacement, targetType, resultConverter);
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public FindAndReplaceWithProjection<T> withOptions(FindAndReplaceOptions options) {
 
 			Assert.notNull(options, "Options must not be null");
 
 			return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
-					options, replacement, targetType);
+					options, replacement, targetType, resultConverter);
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public TerminatingReplace withOptions(ReplaceOptions options) {
 
 			FindAndReplaceOptions target = new FindAndReplaceOptions();
@@ -134,25 +146,27 @@ public TerminatingReplace withOptions(ReplaceOptions options) {
 				target.upsert();
 			}
 			return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
-					target, replacement, targetType);
+					target, replacement, targetType, resultConverter);
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public UpdateWithUpdate<T> matching(Query query) {
 
 			Assert.notNull(query, "Query must not be null");
 
 			return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
-					findAndReplaceOptions, replacement, targetType);
+					findAndReplaceOptions, replacement, targetType, resultConverter);
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public <R> FindAndReplaceWithOptions<R> as(Class<R> resultType) {
 
 			Assert.notNull(resultType, "ResultType must not be null");
 
 			return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
-					findAndReplaceOptions, replacement, resultType);
+					findAndReplaceOptions, replacement, resultType, QueryResultConverter.entity());
 		}
 
 		@Override
@@ -171,22 +185,31 @@ public UpdateResult upsert() {
 		}
 
 		@Override
+		public <R> ExecutableUpdateSupport<S, R> map(QueryResultConverter<? super T, ? extends R> converter) {
+			return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
+					findAndReplaceOptions, replacement, targetType, this.resultConverter.andThen(converter));
+		}
+
+		@Override
+		@SuppressWarnings("NullAway")
 		public @Nullable T findAndModifyValue() {
 
 			return template.findAndModify(query, update,
 					findAndModifyOptions != null ? findAndModifyOptions : new FindAndModifyOptions(), targetType,
-					getCollectionName());
+					getCollectionName(), resultConverter);
 		}
 
 		@Override
+		@SuppressWarnings({ "unchecked", "NullAway" })
 		public @Nullable T findAndReplaceValue() {
 
 			return (T) template.findAndReplace(query, replacement,
-					findAndReplaceOptions != null ? findAndReplaceOptions : FindAndReplaceOptions.empty(), domainType,
-					getCollectionName(), targetType);
+					findAndReplaceOptions != null ? findAndReplaceOptions : FindAndReplaceOptions.empty(), (Class) domainType,
+					getCollectionName(), targetType, (QueryResultConverter) resultConverter);
 		}
 
 		@Override
+		@SuppressWarnings({ "unchecked", "NullAway" })
 		public UpdateResult replaceFirst() {
 
 			if (replacement != null) {
@@ -198,6 +221,7 @@ public UpdateResult replaceFirst() {
 					findAndReplaceOptions != null ? findAndReplaceOptions : ReplaceOptions.none(), getCollectionName());
 		}
 
+		@SuppressWarnings("NullAway")
 		private UpdateResult doUpdate(boolean multi, boolean upsert) {
 			return template.doUpdate(getCollectionName(), query, update, domainType, upsert, multi);
 		}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndModifyOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndModifyOptions.java
index 51a2c5b86a..6e9b775324 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndModifyOptions.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndModifyOptions.java
@@ -17,8 +17,9 @@
 
 import java.util.Optional;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.query.Collation;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 
 /**
  * @author Mark Pollak
@@ -99,16 +100,19 @@ public static FindAndModifyOptions of(@Nullable FindAndModifyOptions source) {
 		return options;
 	}
 
+	@Contract("_ -> this")
 	public FindAndModifyOptions returnNew(boolean returnNew) {
 		this.returnNew = returnNew;
 		return this;
 	}
 
+	@Contract("_ -> this")
 	public FindAndModifyOptions upsert(boolean upsert) {
 		this.upsert = upsert;
 		return this;
 	}
 
+	@Contract("_ -> this")
 	public FindAndModifyOptions remove(boolean remove) {
 		this.remove = remove;
 		return this;
@@ -121,6 +125,7 @@ public FindAndModifyOptions remove(boolean remove) {
 	 * @return this.
 	 * @since 2.0
 	 */
+	@Contract("_ -> this")
 	public FindAndModifyOptions collation(@Nullable Collation collation) {
 
 		this.collation = collation;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndReplaceOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndReplaceOptions.java
index 266a0742c2..2005ba3c6c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndReplaceOptions.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndReplaceOptions.java
@@ -15,6 +15,8 @@
  */
 package org.springframework.data.mongodb.core;
 
+import org.springframework.lang.Contract;
+
 /**
  * Options for
  * <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace</a>.
@@ -95,6 +97,7 @@ public static FindAndReplaceOptions empty() {
 	 *
 	 * @return this.
 	 */
+	@Contract("-> this")
 	public FindAndReplaceOptions returnNew() {
 
 		this.returnNew = true;
@@ -106,6 +109,7 @@ public FindAndReplaceOptions returnNew() {
 	 *
 	 * @return this.
 	 */
+	@Contract("-> this")
 	public FindAndReplaceOptions upsert() {
 
 		super.upsert();
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindPublisherPreparer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindPublisherPreparer.java
index 625a85950e..f04417325c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindPublisherPreparer.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindPublisherPreparer.java
@@ -18,7 +18,7 @@
 import java.util.function.Function;
 
 import org.bson.Document;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.Assert;
 
 import com.mongodb.ReadPreference;
@@ -76,8 +76,7 @@ default FindPublisher<Document> initiateFind(MongoCollection<Document> collectio
 	 * @since 2.2
 	 */
 	@Override
-	@Nullable
-	default ReadPreference getReadPreference() {
+	default @Nullable ReadPreference getReadPreference() {
 		return null;
 	}
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/HintFunction.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/HintFunction.java
index 57abe9a529..043613122a 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/HintFunction.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/HintFunction.java
@@ -18,9 +18,9 @@
 import java.util.function.Function;
 
 import org.bson.conversions.Bson;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.CodecRegistryProvider;
 import org.springframework.data.mongodb.util.BsonUtils;
-import org.springframework.lang.Nullable;
 import org.springframework.util.StringUtils;
 
 /**
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java
index f5856100d0..9f9295bba3 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java
@@ -18,12 +18,10 @@
 import java.util.concurrent.TimeUnit;
 
 import org.bson.Document;
-
+import org.jspecify.annotations.Nullable;
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.data.mongodb.core.index.IndexDefinition;
 import org.springframework.data.mongodb.core.index.IndexInfo;
-import org.springframework.data.mongodb.util.MongoCompatibilityAdapter;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ObjectUtils;
 
 import com.mongodb.client.model.Collation;
@@ -90,9 +88,6 @@ private static Converter<IndexDefinition, IndexOptions> getIndexDefinitionIndexO
 			if (indexOptions.containsKey("bits")) {
 				ops = ops.bits((Integer) indexOptions.get("bits"));
 			}
-			if (indexOptions.containsKey("bucketSize")) {
-				MongoCompatibilityAdapter.indexOptionsAdapter(ops).setBucketSize(((Number) indexOptions.get("bucketSize")).doubleValue());
-			}
 			if (indexOptions.containsKey("default_language")) {
 				ops = ops.defaultLanguage(indexOptions.get("default_language").toString());
 			}
@@ -129,8 +124,7 @@ private static Converter<IndexDefinition, IndexOptions> getIndexDefinitionIndexO
 		};
 	}
 
-	@Nullable
-	public static Collation fromDocument(@Nullable Document source) {
+	public static @Nullable Collation fromDocument(@Nullable Document source) {
 
 		if (source == null) {
 			return null;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappedDocument.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappedDocument.java
index da4766343a..cd9ba90453 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappedDocument.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappedDocument.java
@@ -20,6 +20,7 @@
 
 import org.bson.Document;
 import org.bson.conversions.Bson;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.mapping.FieldName;
 import org.springframework.data.mongodb.core.query.Update;
 import org.springframework.data.mongodb.core.query.UpdateDefinition;
@@ -70,7 +71,7 @@ public boolean hasNonNullId() {
 		return hasId() && document.get(ID_FIELD) != null;
 	}
 
-	public Object getId() {
+	public @Nullable Object getId() {
 		return document.get(ID_FIELD);
 	}
 
@@ -86,7 +87,7 @@ public Bson getIdFilter() {
 		return new Document(ID_FIELD, document.get(ID_FIELD));
 	}
 
-	public Object get(String key) {
+	public @Nullable Object get(String key) {
 		return document.get(key);
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java
index 839f49c7da..396ae1ce8a 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java
@@ -24,6 +24,7 @@
 import java.util.stream.Collectors;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mapping.PersistentProperty;
 import org.springframework.data.mapping.context.MappingContext;
 import org.springframework.data.mongodb.core.convert.MongoConverter;
@@ -31,16 +32,21 @@
 import org.springframework.data.mongodb.core.mapping.Field;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
+import org.springframework.data.mongodb.core.mapping.Queryable;
 import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ArrayJsonSchemaProperty;
 import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty;
 import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ObjectJsonSchemaProperty;
+import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.QueryableJsonSchemaProperty;
 import org.springframework.data.mongodb.core.schema.JsonSchemaObject;
 import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type;
 import org.springframework.data.mongodb.core.schema.JsonSchemaProperty;
 import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
 import org.springframework.data.mongodb.core.schema.MongoJsonSchema.MongoJsonSchemaBuilder;
+import org.springframework.data.mongodb.core.schema.QueryCharacteristic;
+import org.springframework.data.mongodb.core.schema.QueryCharacteristics;
 import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject;
 import org.springframework.data.util.TypeInformation;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.CollectionUtils;
@@ -89,6 +95,7 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
 	}
 
 	@Override
+	@Contract("_ -> new")
 	public MongoJsonSchemaCreator filter(Predicate<JsonSchemaPropertyContext> filter) {
 		return new MappingMongoJsonSchemaCreator(converter, mappingContext, filter, mergeProperties);
 	}
@@ -106,6 +113,7 @@ public PropertySpecifier property(String path) {
 	 * @return new instance of {@link MongoJsonSchemaCreator}.
 	 * @since 3.4
 	 */
+	@Contract("_, _ -> new")
 	public MongoJsonSchemaCreator withTypesFor(String path, Class<?>... types) {
 
 		LinkedMultiValueMap<String, Class<?>> clone = mergeProperties.clone();
@@ -121,29 +129,31 @@ public MongoJsonSchema createSchemaFor(Class<?> type) {
 		MongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(type);
 		MongoJsonSchemaBuilder schemaBuilder = MongoJsonSchema.builder();
 
-		{
-			Encrypted encrypted = entity.findAnnotation(Encrypted.class);
-			if (encrypted != null) {
+		Encrypted encrypted = entity.findAnnotation(Encrypted.class);
+		if (encrypted != null) {
+			schemaBuilder.encryptionMetadata(getEncryptionMetadata(entity, encrypted));
+		}
 
-				Document encryptionMetadata = new Document();
+		List<JsonSchemaProperty> schemaProperties = computePropertiesForEntity(Collections.emptyList(), entity);
+		schemaBuilder.properties(schemaProperties.toArray(new JsonSchemaProperty[0]));
 
-				Collection<Object> encryptionKeyIds = entity.getEncryptionKeyIds();
-				if (!CollectionUtils.isEmpty(encryptionKeyIds)) {
-					encryptionMetadata.append("keyId", encryptionKeyIds);
-				}
+		return schemaBuilder.build();
+	}
 
-				if (StringUtils.hasText(encrypted.algorithm())) {
-					encryptionMetadata.append("algorithm", encrypted.algorithm());
-				}
+	private static Document getEncryptionMetadata(MongoPersistentEntity<?> entity, Encrypted encrypted) {
 
-				schemaBuilder.encryptionMetadata(encryptionMetadata);
-			}
+		Document encryptionMetadata = new Document();
+
+		Collection<Object> encryptionKeyIds = entity.getEncryptionKeyIds();
+		if (!CollectionUtils.isEmpty(encryptionKeyIds)) {
+			encryptionMetadata.append("keyId", encryptionKeyIds);
 		}
 
-		List<JsonSchemaProperty> schemaProperties = computePropertiesForEntity(Collections.emptyList(), entity);
-		schemaBuilder.properties(schemaProperties.toArray(new JsonSchemaProperty[0]));
+		if (StringUtils.hasText(encrypted.algorithm())) {
+			encryptionMetadata.append("algorithm", encrypted.algorithm());
+		}
 
-		return schemaBuilder.build();
+		return encryptionMetadata;
 	}
 
 	private List<JsonSchemaProperty> computePropertiesForEntity(List<MongoPersistentProperty> path,
@@ -176,6 +186,7 @@ private List<JsonSchemaProperty> computePropertiesForEntity(List<MongoPersistent
 		return schemaProperties;
 	}
 
+	@SuppressWarnings("NullAway")
 	private JsonSchemaProperty computeSchemaForProperty(List<MongoPersistentProperty> path) {
 
 		String stringPath = path.stream().map(MongoPersistentProperty::getName).collect(Collectors.joining("."));
@@ -185,8 +196,8 @@ private JsonSchemaProperty computeSchemaForProperty(List<MongoPersistentProperty
 		Class<?> rawTargetType = computeTargetType(property); // target type before conversion
 		Class<?> targetType = converter.getTypeMapper().getWriteTargetTypeFor(rawTargetType); // conversion target type
 
-
-		if ((rawTargetType.isPrimitive() || ClassUtils.isPrimitiveArray(rawTargetType)) && targetType == Object.class || ClassUtils.isAssignable(targetType, rawTargetType) ) {
+		if ((rawTargetType.isPrimitive() || ClassUtils.isPrimitiveArray(rawTargetType)) && targetType == Object.class
+				|| ClassUtils.isAssignable(targetType, rawTargetType)) {
 			targetType = rawTargetType;
 		}
 
@@ -291,7 +302,36 @@ private JsonSchemaProperty applyEncryptionDataIfNecessary(MongoPersistentPropert
 		if (!ObjectUtils.isEmpty(encrypted.keyId())) {
 			enc = enc.keys(property.getEncryptionKeyIds());
 		}
-		return enc;
+
+		Queryable queryable = property.findAnnotation(Queryable.class);
+		if (queryable == null || !StringUtils.hasText(queryable.queryType())) {
+			return enc;
+		}
+
+		QueryCharacteristic characteristic = new QueryCharacteristic() {
+
+			@Override
+			public String queryType() {
+				return queryable.queryType();
+			}
+
+			@Override
+			public Document toDocument() {
+
+				Document options = QueryCharacteristic.super.toDocument();
+
+				if (queryable.contentionFactor() >= 0) {
+					options.put("contention", queryable.contentionFactor());
+				}
+
+				if (StringUtils.hasText(queryable.queryAttributes())) {
+					options.putAll(Document.parse(queryable.queryAttributes()));
+				}
+
+				return options;
+			}
+		};
+		return new QueryableJsonSchemaProperty(enc, QueryCharacteristics.of(characteristic));
 	}
 
 	private JsonSchemaProperty createObjectSchemaPropertyForEntity(List<MongoPersistentProperty> path,
@@ -337,7 +377,9 @@ private TypedJsonSchemaObject createSchemaObject(Object type, Collection<?> poss
 		return schemaObject;
 	}
 
-	private String computePropertyFieldName(PersistentProperty<?> property) {
+	private String computePropertyFieldName(@Nullable PersistentProperty<?> property) {
+
+		Assert.notNull(property, "Property must not be null");
 
 		return property instanceof MongoPersistentProperty mongoPersistentProperty ? mongoPersistentProperty.getFieldName()
 				: property.getName();
@@ -409,7 +451,8 @@ public MongoPersistentProperty getProperty() {
 		}
 
 		@Override
-		public <T> MongoPersistentEntity<T> resolveEntity(MongoPersistentProperty property) {
+		@SuppressWarnings("unchecked")
+		public <T> @Nullable MongoPersistentEntity<T> resolveEntity(MongoPersistentProperty property) {
 			return (MongoPersistentEntity<T>) mappingContext.getPersistentEntity(property);
 		}
 	}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoAction.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoAction.java
index fdfeaa81ad..c827c5b8a9 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoAction.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoAction.java
@@ -16,7 +16,7 @@
 package org.springframework.data.mongodb.core;
 
 import org.bson.Document;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.Assert;
 
 import com.mongodb.WriteConcern;
@@ -72,28 +72,23 @@ public String getCollectionName() {
 		return collectionName;
 	}
 
-	@Nullable
-	public WriteConcern getDefaultWriteConcern() {
+	public @Nullable WriteConcern getDefaultWriteConcern() {
 		return defaultWriteConcern;
 	}
 
-	@Nullable
-	public Class<?> getEntityType() {
+	public @Nullable Class<?> getEntityType() {
 		return entityType;
 	}
 
-	@Nullable
-	public MongoActionOperation getMongoActionOperation() {
+	public @Nullable MongoActionOperation getMongoActionOperation() {
 		return mongoActionOperation;
 	}
 
-	@Nullable
-	public Document getQuery() {
+	public @Nullable Document getQuery() {
 		return query;
 	}
 
-	@Nullable
-	public Document getDocument() {
+	public @Nullable Document getDocument() {
 		return document;
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java
index c5fee9cf54..9210dd85ec 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java
@@ -24,11 +24,11 @@
 import java.util.stream.Collectors;
 
 import org.bson.UuidRepresentation;
+import org.jspecify.annotations.Nullable;
 import org.springframework.beans.factory.config.AbstractFactoryBean;
 import org.springframework.dao.DataAccessException;
 import org.springframework.dao.support.PersistenceExceptionTranslator;
 import org.springframework.data.mongodb.SpringDataMongoDB;
-import org.springframework.lang.Nullable;
 import org.springframework.util.CollectionUtils;
 import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
@@ -78,7 +78,7 @@ public void setMongoClientSettings(@Nullable MongoClientSettings mongoClientOpti
 	 *
 	 * @param credential can be {@literal null}.
 	 */
-	public void setCredential(@Nullable MongoCredential[] credential) {
+	public void setCredential(MongoCredential @Nullable[] credential) {
 		this.credential = Arrays.asList(credential);
 	}
 
@@ -119,8 +119,7 @@ public void setExceptionTranslator(@Nullable PersistenceExceptionTranslator exce
 	}
 
 	@Override
-	@Nullable
-	public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
+	public @Nullable DataAccessException translateExceptionIfPossible(RuntimeException ex) {
 		return exceptionTranslator.translateExceptionIfPossible(ex);
 	}
 
@@ -316,13 +315,13 @@ private <T> void applySettings(Consumer<T> settingsBuilder, @Nullable T value) {
 		settingsBuilder.accept(value);
 	}
 
-	private <S, T> T computeSettingsValue(Function<S, T> function, S defaultValueHolder, S settingsValueHolder,
+	private <S, T> @Nullable T computeSettingsValue(Function<S, T> function, S defaultValueHolder, S settingsValueHolder,
 			@Nullable T connectionStringValue) {
 		return computeSettingsValue(function.apply(defaultValueHolder), function.apply(settingsValueHolder),
 				connectionStringValue);
 	}
 
-	private <T> T computeSettingsValue(T defaultValue, T fromSettings, T fromConnectionString) {
+	private <T> @Nullable T computeSettingsValue(@Nullable T defaultValue, T fromSettings, @Nullable T fromConnectionString) {
 
 		boolean fromSettingsIsDefault = ObjectUtils.nullSafeEquals(defaultValue, fromSettings);
 		boolean fromConnectionStringIsDefault = ObjectUtils.nullSafeEquals(defaultValue, fromConnectionString);
@@ -337,7 +336,7 @@ private MongoClient createMongoClient(MongoClientSettings settings) throws Unkno
 		return MongoClients.create(settings, SpringDataMongoDB.driverInformation());
 	}
 
-	private String getOrDefault(Object value, String defaultValue) {
+	private String getOrDefault(@Nullable Object value, String defaultValue) {
 
 		if(value == null) {
 			return defaultValue;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java
index 02913b4303..813d3a4a04 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java
@@ -25,10 +25,8 @@
 
 import org.bson.UuidRepresentation;
 import org.bson.codecs.configuration.CodecRegistry;
-
+import org.jspecify.annotations.Nullable;
 import org.springframework.beans.factory.config.AbstractFactoryBean;
-import org.springframework.data.mongodb.util.MongoCompatibilityAdapter;
-import org.springframework.lang.Nullable;
 import org.springframework.util.CollectionUtils;
 import org.springframework.util.StringUtils;
 
@@ -57,8 +55,6 @@ public class MongoClientSettingsFactoryBean extends AbstractFactoryBean<MongoCli
 
 	private CodecRegistry codecRegistry = DEFAULT_MONGO_SETTINGS.getCodecRegistry();
 
-	@Nullable private Object streamFactoryFactory = MongoCompatibilityAdapter
-			.clientSettingsAdapter(DEFAULT_MONGO_SETTINGS).getStreamFactoryFactory();
 	@Nullable private TransportSettings transportSettings;
 
 	private ReadPreference readPreference = DEFAULT_MONGO_SETTINGS.getReadPreference();
@@ -371,16 +367,6 @@ public void setReadPreference(ReadPreference readPreference) {
 		this.readPreference = readPreference;
 	}
 
-	/**
-	 * @param streamFactoryFactory
-	 * @deprecated since 4.3, will be removed in the MongoDB 5.0 driver in favor of
-	 *             {@code com.mongodb.connection.TransportSettings}.
-	 */
-	@Deprecated(since = "4.3")
-	public void setStreamFactoryFactory(Object streamFactoryFactory) {
-		this.streamFactoryFactory = streamFactoryFactory;
-	}
-
 	public void setTransportSettings(@Nullable TransportSettings transportSettings) {
 		this.transportSettings = transportSettings;
 	}
@@ -492,10 +478,6 @@ protected MongoClientSettings createInstance() {
 			builder.transportSettings(transportSettings);
 		}
 
-		if (streamFactoryFactory != null) {
-			MongoCompatibilityAdapter.clientSettingsBuilderAdapter(builder).setStreamFactoryFactory(streamFactoryFactory);
-		}
-
 		if (retryReads != null) {
 			builder = builder.retryReads(retryReads);
 		}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoDatabaseFactorySupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoDatabaseFactorySupport.java
index eab6b5d7f4..0a62b7aa49 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoDatabaseFactorySupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoDatabaseFactorySupport.java
@@ -15,12 +15,13 @@
  */
 package org.springframework.data.mongodb.core;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.aop.framework.ProxyFactory;
 import org.springframework.dao.DataAccessException;
 import org.springframework.dao.support.PersistenceExceptionTranslator;
 import org.springframework.data.mongodb.MongoDatabaseFactory;
 import org.springframework.data.mongodb.SessionAwareMethodInterceptor;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 
@@ -132,6 +133,7 @@ public void destroy() throws Exception {
 	}
 
 	@Override
+	@Contract("_ -> new")
 	public MongoDatabaseFactory withSession(ClientSession session) {
 		return new MongoDatabaseFactorySupport.ClientSessionBoundMongoDbFactory(session, this);
 	}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoEncryptionSettingsFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoEncryptionSettingsFactoryBean.java
index 7aef5a3a82..f361b19bba 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoEncryptionSettingsFactoryBean.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoEncryptionSettingsFactoryBean.java
@@ -19,8 +19,8 @@
 import java.util.Map;
 
 import org.bson.BsonDocument;
+import org.jspecify.annotations.Nullable;
 import org.springframework.beans.factory.FactoryBean;
-import org.springframework.lang.Nullable;
 
 import com.mongodb.AutoEncryptionSettings;
 import com.mongodb.MongoClientSettings;
@@ -34,11 +34,11 @@
 public class MongoEncryptionSettingsFactoryBean implements FactoryBean<AutoEncryptionSettings> {
 
 	private boolean bypassAutoEncryption;
-	private String keyVaultNamespace;
-	private Map<String, Object> extraOptions;
-	private MongoClientSettings keyVaultClientSettings;
-	private Map<String, Map<String, Object>> kmsProviders;
-	private Map<String, BsonDocument> schemaMap;
+	private @Nullable String keyVaultNamespace;
+	private @Nullable Map<String, Object> extraOptions;
+	private @Nullable MongoClientSettings keyVaultClientSettings;
+	private @Nullable Map<String, Map<String, Object>> kmsProviders;
+	private @Nullable Map<String, BsonDocument> schemaMap;
 
 	/**
 	 * @param bypassAutoEncryption
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java
index 1ec7d3ffc0..2bde873c2f 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java
@@ -18,7 +18,7 @@
 import java.util.Set;
 
 import org.bson.BsonInvalidOperationException;
-
+import org.jspecify.annotations.Nullable;
 import org.springframework.dao.DataAccessException;
 import org.springframework.dao.DataAccessResourceFailureException;
 import org.springframework.dao.DataIntegrityViolationException;
@@ -31,7 +31,6 @@
 import org.springframework.data.mongodb.TransientClientSessionException;
 import org.springframework.data.mongodb.UncategorizedMongoDbException;
 import org.springframework.data.mongodb.util.MongoDbErrorCodes;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ClassUtils;
 
 import com.mongodb.MongoBulkWriteException;
@@ -69,12 +68,12 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
 	private static final Set<String> SECURITY_EXCEPTIONS = Set.of("MongoCryptException");
 
 	@Override
-	@Nullable
-	public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
+	public @Nullable DataAccessException translateExceptionIfPossible(RuntimeException ex) {
 		return doTranslateException(ex);
 	}
 
 	@Nullable
+	@SuppressWarnings("NullAway")
 	DataAccessException doTranslateException(RuntimeException ex) {
 
 		// Check for well-known MongoException subclasses.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoJsonSchemaCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoJsonSchemaCreator.java
index 66b1cf209e..84c395bf2f 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoJsonSchemaCreator.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoJsonSchemaCreator.java
@@ -139,8 +139,7 @@ interface JsonSchemaPropertyContext {
 		 * @return {@literal null} if the property is not an entity. It is nevertheless recommend to check
 		 *         {@link PersistentProperty#isEntity()} first.
 		 */
-		@Nullable
-		<T> MongoPersistentEntity<T> resolveEntity(MongoPersistentProperty property);
+		<T> @Nullable MongoPersistentEntity<T> resolveEntity(MongoPersistentProperty property);
 
 	}
 
@@ -162,6 +161,7 @@ public boolean test(JsonSchemaPropertyContext context) {
 				return extracted(context.getProperty(), context);
 			}
 
+			@SuppressWarnings("NullAway")
 			private boolean extracted(MongoPersistentProperty property, JsonSchemaPropertyContext context) {
 				if (property.isAnnotationPresent(Encrypted.class)) {
 					return true;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java
index 65396bc7fe..6753f31c1a 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java
@@ -24,6 +24,7 @@
 import java.util.stream.Stream;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.domain.KeysetScrollPosition;
 import org.springframework.data.domain.Sort;
 import org.springframework.data.domain.Window;
@@ -49,7 +50,6 @@
 import org.springframework.data.mongodb.core.query.Update;
 import org.springframework.data.mongodb.core.query.UpdateDefinition;
 import org.springframework.data.util.Lock;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 
@@ -196,7 +196,8 @@ default SessionScoped withSession(Supplier<ClientSession> sessionProvider) {
 			private @Nullable ClientSession session;
 
 			@Override
-			public <T> T execute(SessionCallback<T> action, Consumer<ClientSession> onComplete) {
+			@SuppressWarnings("NullAway")
+			public <T> @Nullable T execute(SessionCallback<T> action, Consumer<ClientSession> onComplete) {
 
 				lock.executeWithoutResult(() -> {
 
@@ -733,8 +734,7 @@ <T> MapReduceResults<T> mapReduce(Query query, String inputCollectionName, Strin
 	 * @param entityClass the parametrized type of the returned list.
 	 * @return the converted object.
 	 */
-	@Nullable
-	<T> T findOne(Query query, Class<T> entityClass);
+	<T> @Nullable T findOne(Query query, Class<T> entityClass);
 
 	/**
 	 * Map the results of an ad-hoc query on the specified collection to a single instance of an object of the specified
@@ -750,8 +750,7 @@ <T> MapReduceResults<T> mapReduce(Query query, String inputCollectionName, Strin
 	 * @param collectionName name of the collection to retrieve the objects from.
 	 * @return the converted object.
 	 */
-	@Nullable
-	<T> T findOne(Query query, Class<T> entityClass, String collectionName);
+	<T> @Nullable T findOne(Query query, Class<T> entityClass, String collectionName);
 
 	/**
 	 * Determine result of given {@link Query} contains at least one element. <br />
@@ -823,7 +822,7 @@ <T> MapReduceResults<T> mapReduce(Query query, String inputCollectionName, Strin
 	 * <p>
 	 * When using {@link KeysetScrollPosition}, make sure to use non-nullable {@link org.springframework.data.domain.Sort
 	 * sort properties} as MongoDB does not support criteria to reconstruct a query result from absent document fields or
-	 * {@code null} values through {@code $gt/$lt} operators.
+	 * {@literal null} values through {@code $gt/$lt} operators.
 	 *
 	 * @param query the query class that specifies the criteria used to find a document and also an optional fields
 	 *          specification. Must not be {@literal null}.
@@ -848,7 +847,7 @@ <T> MapReduceResults<T> mapReduce(Query query, String inputCollectionName, Strin
 	 * <p>
 	 * When using {@link KeysetScrollPosition}, make sure to use non-nullable {@link org.springframework.data.domain.Sort
 	 * sort properties} as MongoDB does not support criteria to reconstruct a query result from absent document fields or
-	 * {@code null} values through {@code $gt/$lt} operators.
+	 * {@literal null} values through {@code $gt/$lt} operators.
 	 *
 	 * @param query the query class that specifies the criteria used to find a document and also an optional fields
 	 *          specification. Must not be {@literal null}.
@@ -871,8 +870,7 @@ <T> MapReduceResults<T> mapReduce(Query query, String inputCollectionName, Strin
 	 * @param entityClass the type the document shall be converted into. Must not be {@literal null}.
 	 * @return the document with the given id mapped onto the given target class.
 	 */
-	@Nullable
-	<T> T findById(Object id, Class<T> entityClass);
+	<T> @Nullable T findById(Object id, Class<T> entityClass);
 
 	/**
 	 * Returns the document with the given id from the given collection mapped onto the given target class.
@@ -882,8 +880,7 @@ <T> MapReduceResults<T> mapReduce(Query query, String inputCollectionName, Strin
 	 * @param collectionName the collection to query for the document.
 	 * @return he converted object or {@literal null} if document does not exist.
 	 */
-	@Nullable
-	<T> T findById(Object id, Class<T> entityClass, String collectionName);
+	<T> @Nullable T findById(Object id, Class<T> entityClass, String collectionName);
 
 	/**
 	 * Finds the distinct values for a specified {@literal field} across a single {@link MongoCollection} or view and
@@ -960,8 +957,7 @@ default <T> List<T> findDistinct(Query query, String field, String collection, C
 	 * @see Update
 	 * @see AggregationUpdate
 	 */
-	@Nullable
-	<T> T findAndModify(Query query, UpdateDefinition update, Class<T> entityClass);
+	<T> @Nullable T findAndModify(Query query, UpdateDefinition update, Class<T> entityClass);
 
 	/**
 	 * Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify </a>
@@ -980,8 +976,7 @@ default <T> List<T> findDistinct(Query query, String field, String collection, C
 	 * @see Update
 	 * @see AggregationUpdate
 	 */
-	@Nullable
-	<T> T findAndModify(Query query, UpdateDefinition update, Class<T> entityClass, String collectionName);
+	<T> @Nullable T findAndModify(Query query, UpdateDefinition update, Class<T> entityClass, String collectionName);
 
 	/**
 	 * Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify </a>
@@ -1003,8 +998,7 @@ default <T> List<T> findDistinct(Query query, String field, String collection, C
 	 * @see Update
 	 * @see AggregationUpdate
 	 */
-	@Nullable
-	<T> T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class<T> entityClass);
+	<T> @Nullable T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class<T> entityClass);
 
 	/**
 	 * Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify </a>
@@ -1027,8 +1021,7 @@ default <T> List<T> findDistinct(Query query, String field, String collection, C
 	 * @see Update
 	 * @see AggregationUpdate
 	 */
-	@Nullable
-	<T> T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class<T> entityClass,
+	<T> @Nullable T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class<T> entityClass,
 			String collectionName);
 
 	/**
@@ -1048,8 +1041,7 @@ <T> T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions o
 	 *           {@link #getCollectionName(Class) derived} from the given replacement value.
 	 * @since 2.1
 	 */
-	@Nullable
-	default <T> T findAndReplace(Query query, T replacement) {
+	default <T> @Nullable T findAndReplace(Query query, T replacement) {
 		return findAndReplace(query, replacement, FindAndReplaceOptions.empty());
 	}
 
@@ -1068,8 +1060,7 @@ default <T> T findAndReplace(Query query, T replacement) {
 	 * @return the converted object that was updated or {@literal null}, if not found.
 	 * @since 2.1
 	 */
-	@Nullable
-	default <T> T findAndReplace(Query query, T replacement, String collectionName) {
+	default <T> @Nullable T findAndReplace(Query query, T replacement, String collectionName) {
 		return findAndReplace(query, replacement, FindAndReplaceOptions.empty(), collectionName);
 	}
 
@@ -1091,8 +1082,7 @@ default <T> T findAndReplace(Query query, T replacement, String collectionName)
 	 *           {@link #getCollectionName(Class) derived} from the given replacement value.
 	 * @since 2.1
 	 */
-	@Nullable
-	default <T> T findAndReplace(Query query, T replacement, FindAndReplaceOptions options) {
+	default <T> @Nullable T findAndReplace(Query query, T replacement, FindAndReplaceOptions options) {
 		return findAndReplace(query, replacement, options, getCollectionName(ClassUtils.getUserClass(replacement)));
 	}
 
@@ -1112,8 +1102,7 @@ default <T> T findAndReplace(Query query, T replacement, FindAndReplaceOptions o
 	 *         as it is after the update.
 	 * @since 2.1
 	 */
-	@Nullable
-	default <T> T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, String collectionName) {
+	default <T> @Nullable T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, String collectionName) {
 
 		Assert.notNull(replacement, "Replacement must not be null");
 		return findAndReplace(query, replacement, options, (Class<T>) ClassUtils.getUserClass(replacement), collectionName);
@@ -1137,8 +1126,7 @@ default <T> T findAndReplace(Query query, T replacement, FindAndReplaceOptions o
 	 *         as it is after the update.
 	 * @since 2.1
 	 */
-	@Nullable
-	default <T> T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, Class<T> entityType,
+	default <T> @Nullable T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, Class<T> entityType,
 			String collectionName) {
 
 		return findAndReplace(query, replacement, options, entityType, collectionName, entityType);
@@ -1166,8 +1154,7 @@ default <T> T findAndReplace(Query query, T replacement, FindAndReplaceOptions o
 	 *           {@link #getCollectionName(Class) derived} from the given replacement value.
 	 * @since 2.1
 	 */
-	@Nullable
-	default <S, T> T findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class<S> entityType,
+	default <S, T> @Nullable T findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class<S> entityType,
 			Class<T> resultType) {
 
 		return findAndReplace(query, replacement, options, entityType,
@@ -1194,8 +1181,7 @@ default <S, T> T findAndReplace(Query query, S replacement, FindAndReplaceOption
 	 *         as it is after the update.
 	 * @since 2.1
 	 */
-	@Nullable
-	<S, T> T findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class<S> entityType,
+	<S, T> @Nullable T findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class<S> entityType,
 			String collectionName, Class<T> resultType);
 
 	/**
@@ -1211,8 +1197,7 @@ <S, T> T findAndReplace(Query query, S replacement, FindAndReplaceOptions option
 	 * @param entityClass the parametrized type of the returned list.
 	 * @return the converted object
 	 */
-	@Nullable
-	<T> T findAndRemove(Query query, Class<T> entityClass);
+	<T> @Nullable T findAndRemove(Query query, Class<T> entityClass);
 
 	/**
 	 * Map the results of an ad-hoc query on the specified collection to a single instance of an object of the specified
@@ -1229,8 +1214,7 @@ <S, T> T findAndReplace(Query query, S replacement, FindAndReplaceOptions option
 	 * @param collectionName name of the collection to retrieve the objects from.
 	 * @return the converted object.
 	 */
-	@Nullable
-	<T> T findAndRemove(Query query, Class<T> entityClass, String collectionName);
+	<T> @Nullable T findAndRemove(Query query, Class<T> entityClass, String collectionName);
 
 	/**
 	 * Returns the number of documents for the given {@link Query} by querying the collection of the given entity class.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoServerApiFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoServerApiFactoryBean.java
index 37001faa4e..574c0c8931 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoServerApiFactoryBean.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoServerApiFactoryBean.java
@@ -15,8 +15,8 @@
  */
 package org.springframework.data.mongodb.core;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.beans.factory.FactoryBean;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ObjectUtils;
 
 import com.mongodb.ServerApi;
@@ -31,7 +31,7 @@
  */
 public class MongoServerApiFactoryBean implements FactoryBean<ServerApi> {
 
-	private String version;
+	private @Nullable String version;
 	private @Nullable Boolean deprecationErrors;
 	private @Nullable Boolean strict;
 
@@ -59,9 +59,8 @@ public void setStrict(@Nullable Boolean strict) {
 		this.strict = strict;
 	}
 
-	@Nullable
 	@Override
-	public ServerApi getObject() throws Exception {
+	public @Nullable ServerApi getObject() throws Exception {
 
 		Builder builder = ServerApi.builder().version(version());
 
@@ -81,6 +80,11 @@ public Class<?> getObjectType() {
 	}
 
 	private ServerApiVersion version() {
+
+		if(version == null) {
+			return ServerApiVersion.V1;
+		}
+
 		try {
 			// lookup by name eg. 'V1'
 			return ObjectUtils.caseInsensitiveValueOf(ServerApiVersion.values(), version);
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java
index 67ef3a3081..ab03b41424 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java
@@ -30,6 +30,7 @@
 import org.apache.commons.logging.LogFactory;
 import org.bson.Document;
 import org.bson.conversions.Bson;
+import org.jspecify.annotations.Nullable;
 
 import org.springframework.beans.BeansException;
 import org.springframework.context.ApplicationContext;
@@ -100,6 +101,7 @@
 import org.springframework.data.mongodb.core.mapreduce.MapReduceResults;
 import org.springframework.data.mongodb.core.query.BasicQuery;
 import org.springframework.data.mongodb.core.query.Collation;
+import org.springframework.data.mongodb.core.query.Criteria;
 import org.springframework.data.mongodb.core.query.Meta;
 import org.springframework.data.mongodb.core.query.NearQuery;
 import org.springframework.data.mongodb.core.query.Query;
@@ -107,11 +109,11 @@
 import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter;
 import org.springframework.data.mongodb.core.timeseries.Granularity;
 import org.springframework.data.mongodb.core.validation.Validator;
-import org.springframework.data.mongodb.util.MongoCompatibilityAdapter;
 import org.springframework.data.projection.EntityProjection;
 import org.springframework.data.util.CloseableIterator;
+import org.springframework.data.util.Lazy;
 import org.springframework.data.util.Optionals;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.CollectionUtils;
@@ -343,7 +345,7 @@ public boolean hasReadPreference() {
 	}
 
 	@Override
-	public ReadPreference getReadPreference() {
+	public @Nullable ReadPreference getReadPreference() {
 		return this.readPreference;
 	}
 
@@ -479,15 +481,21 @@ public <T> Stream<T> stream(Query query, Class<T> entityType, String collectionN
 		return doStream(query, entityType, collectionName, entityType);
 	}
 
-	@SuppressWarnings("ConstantConditions")
+	@SuppressWarnings({ "ConstantConditions", "NullAway" })
 	protected <T> Stream<T> doStream(Query query, Class<?> entityType, String collectionName, Class<T> returnType) {
+		return doStream(query, entityType, collectionName, returnType, QueryResultConverter.entity());
+	}
+
+	@SuppressWarnings({"ConstantConditions", "NullAway"})
+	<T, R> Stream<R> doStream(Query query, Class<?> entityType, String collectionName, Class<T> returnType,
+			QueryResultConverter<? super T, ? extends R> resultConverter) {
 
 		Assert.notNull(query, "Query must not be null");
 		Assert.notNull(entityType, "Entity type must not be null");
 		Assert.hasText(collectionName, "Collection name must not be null or empty");
 		Assert.notNull(returnType, "ReturnType must not be null");
 
-		return execute(collectionName, (CollectionCallback<Stream<T>>) collection -> {
+		return execute(collectionName, (CollectionCallback<Stream<R>>) collection -> {
 
 			MongoPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(entityType);
 
@@ -501,8 +509,10 @@ protected <T> Stream<T> doStream(Query query, Class<?> entityType, String collec
 			FindIterable<Document> cursor = new QueryCursorPreparer(query, entityType).initiateFind(collection,
 					col -> readPreference.prepare(col).find(mappedQuery, Document.class).projection(mappedFields));
 
+			DocumentCallback<R> resultReader = getResultReader(projection, collectionName, resultConverter);
+
 			return new CloseableIterableCursorAdapter<>(cursor, exceptionTranslator,
-					new ProjectingReadCallback<>(mongoConverter, projection, collectionName)).stream();
+					resultReader).stream();
 		});
 	}
 
@@ -512,7 +522,7 @@ public String getCollectionName(Class<?> entityClass) {
 	}
 
 	@Override
-	@SuppressWarnings("ConstantConditions")
+	@SuppressWarnings({ "ConstantConditions", "NullAway" })
 	public Document executeCommand(String jsonCommand) {
 
 		Assert.hasText(jsonCommand, "JsonCommand must not be null nor empty");
@@ -521,7 +531,7 @@ public Document executeCommand(String jsonCommand) {
 	}
 
 	@Override
-	@SuppressWarnings("ConstantConditions")
+	@SuppressWarnings({ "ConstantConditions", "NullAway" })
 	public Document executeCommand(Document command) {
 
 		Assert.notNull(command, "Command must not be null");
@@ -530,7 +540,7 @@ public Document executeCommand(Document command) {
 	}
 
 	@Override
-	@SuppressWarnings("ConstantConditions")
+	@SuppressWarnings({ "ConstantConditions", "NullAway" })
 	public Document executeCommand(Document command, @Nullable ReadPreference readPreference) {
 
 		Assert.notNull(command, "Command must not be null");
@@ -577,7 +587,7 @@ protected void executeQuery(Query query, String collectionName, DocumentCallback
 	}
 
 	@Override
-	public <T> T execute(DbCallback<T> action) {
+	public <T> @Nullable T execute(DbCallback<T> action) {
 
 		Assert.notNull(action, "DbCallback must not be null");
 
@@ -590,14 +600,14 @@ public <T> T execute(DbCallback<T> action) {
 	}
 
 	@Override
-	public <T> T execute(Class<?> entityClass, CollectionCallback<T> callback) {
+	public <T> @Nullable T execute(Class<?> entityClass, CollectionCallback<T> callback) {
 
 		Assert.notNull(entityClass, "EntityClass must not be null");
 		return execute(getCollectionName(entityClass), callback);
 	}
 
 	@Override
-	public <T> T execute(String collectionName, CollectionCallback<T> callback) {
+	public <T> @Nullable T execute(String collectionName, CollectionCallback<T> callback) {
 
 		Assert.notNull(collectionName, "CollectionName must not be null");
 		Assert.notNull(callback, "CollectionCallback must not be null");
@@ -619,6 +629,7 @@ public SessionScoped withSession(ClientSessionOptions options) {
 	}
 
 	@Override
+	@Contract("_ -> new")
 	public MongoTemplate withSession(ClientSession session) {
 
 		Assert.notNull(session, "ClientSession must not be null");
@@ -692,6 +703,7 @@ private MongoCollection<Document> createView(String name, String source, Aggrega
 		return doCreateView(name, source, aggregation.getAggregationPipeline(), options);
 	}
 
+	@SuppressWarnings("NullAway")
 	protected MongoCollection<Document> doCreateView(String name, String source, List<Document> pipeline,
 			@Nullable ViewOptions options) {
 
@@ -707,8 +719,9 @@ protected MongoCollection<Document> doCreateView(String name, String source, Lis
 	}
 
 	@Override
-	@SuppressWarnings("ConstantConditions")
-	public MongoCollection<Document> getCollection(String collectionName) {
+	@SuppressWarnings({ "ConstantConditions", "NullAway" })
+	@Contract("null -> fail")
+	public MongoCollection<Document> getCollection(@Nullable String collectionName) {
 
 		Assert.notNull(collectionName, "CollectionName must not be null");
 
@@ -721,14 +734,14 @@ public <T> boolean collectionExists(Class<T> entityClass) {
 	}
 
 	@Override
-	@SuppressWarnings("ConstantConditions")
+	@SuppressWarnings({ "ConstantConditions", "NullAway" })
 	public boolean collectionExists(String collectionName) {
 
 		Assert.notNull(collectionName, "CollectionName must not be null");
 
 		return execute(db -> {
 
-			for (String name : MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(db).listCollectionNames()) {
+			for (String name : db.listCollectionNames()) {
 				if (name.equals(collectionName)) {
 					return true;
 				}
@@ -855,7 +868,7 @@ public boolean exists(Query query, String collectionName) {
 	}
 
 	@Override
-	@SuppressWarnings("ConstantConditions")
+	@SuppressWarnings({ "ConstantConditions", "NullAway" })
 	public boolean exists(Query query, @Nullable Class<?> entityClass, String collectionName) {
 
 		if (query == null) {
@@ -898,10 +911,11 @@ public <T> Window<T> scroll(Query query, Class<T> entityType) {
 
 	@Override
 	public <T> Window<T> scroll(Query query, Class<T> entityType, String collectionName) {
-		return doScroll(query, entityType, entityType, collectionName);
+		return doScroll(query, entityType, entityType, QueryResultConverter.entity(), collectionName);
 	}
 
-	<T> Window<T> doScroll(Query query, Class<?> sourceClass, Class<T> targetClass, String collectionName) {
+	<T, R> Window<R> doScroll(Query query, Class<?> sourceClass, Class<T> targetClass,
+			QueryResultConverter<? super T, ? extends R> resultConverter, String collectionName) {
 
 		Assert.notNull(query, "Query must not be null");
 		Assert.notNull(collectionName, "CollectionName must not be null");
@@ -909,7 +923,7 @@ <T> Window<T> doScroll(Query query, Class<?> sourceClass, Class<T> targetClass,
 		Assert.notNull(targetClass, "Target type must not be null");
 
 		EntityProjection<T, ?> projection = operations.introspectProjection(targetClass, sourceClass);
-		ProjectingReadCallback<?, T> callback = new ProjectingReadCallback<>(mongoConverter, projection, collectionName);
+		DocumentCallback<R> callback = getResultReader(projection, collectionName, resultConverter);
 		int limit = query.isLimited() ? query.getLimit() + 1 : Integer.MAX_VALUE;
 
 		if (query.hasKeyset()) {
@@ -917,14 +931,14 @@ <T> Window<T> doScroll(Query query, Class<?> sourceClass, Class<T> targetClass,
 			KeysetScrollQuery keysetPaginationQuery = ScrollUtils.createKeysetPaginationQuery(query,
 					operations.getIdPropertyName(sourceClass));
 
-			List<T> result = doFind(collectionName, createDelegate(query), keysetPaginationQuery.query(),
+			List<R> result = doFind(collectionName, createDelegate(query), keysetPaginationQuery.query(),
 					keysetPaginationQuery.fields(), sourceClass,
 					new QueryCursorPreparer(query, keysetPaginationQuery.sort(), limit, 0, sourceClass), callback);
 
 			return ScrollUtils.createWindow(query, result, sourceClass, operations);
 		}
 
-		List<T> result = doFind(collectionName, createDelegate(query), query.getQueryObject(), query.getFieldsObject(),
+		List<R> result = doFind(collectionName, createDelegate(query), query.getQueryObject(), query.getFieldsObject(),
 				sourceClass, new QueryCursorPreparer(query, query.getSortObject(), limit, query.getSkip(), sourceClass),
 				callback);
 
@@ -957,7 +971,7 @@ public <T> List<T> findDistinct(Query query, String field, Class<?> entityClass,
 	}
 
 	@Override
-	@SuppressWarnings("unchecked")
+	@SuppressWarnings({ "unchecked", "NullAway" })
 	public <T> List<T> findDistinct(Query query, String field, String collectionName, Class<?> entityClass,
 			Class<T> resultClass) {
 
@@ -1016,6 +1030,11 @@ public <T> GeoResults<T> geoNear(NearQuery near, Class<T> domainType, String col
 	}
 
 	public <T> GeoResults<T> geoNear(NearQuery near, Class<?> domainType, String collectionName, Class<T> returnType) {
+		return doGeoNear(near, domainType, collectionName, returnType, QueryResultConverter.entity());
+	}
+
+	<T, R> GeoResults<R> doGeoNear(NearQuery near, Class<?> domainType, String collectionName, Class<T> returnType,
+			QueryResultConverter<? super T, ? extends R> resultConverter) {
 
 		if (near == null) {
 			throw new InvalidDataAccessApiUsageException("NearQuery must not be null");
@@ -1047,48 +1066,50 @@ public <T> GeoResults<T> geoNear(NearQuery near, Class<?> domainType, String col
 		AggregationResults<Document> results = aggregate($geoNear, collection, Document.class);
 		EntityProjection<T, ?> projection = operations.introspectProjection(returnType, domainType);
 
-		DocumentCallback<GeoResult<T>> callback = new GeoNearResultDocumentCallback<>(distanceField,
-				new ProjectingReadCallback<>(mongoConverter, projection, collection), near.getMetric());
+		DocumentCallback<GeoResult<R>> callback = new GeoNearResultDocumentCallback<>(distanceField,
+				getResultReader(projection, collectionName, resultConverter), near.getMetric());
 
-		List<GeoResult<T>> result = new ArrayList<>(results.getMappedResults().size());
+		List<GeoResult<R>> result = new ArrayList<>(results.getMappedResults().size());
 
 		BigDecimal aggregate = BigDecimal.ZERO;
 		for (Document element : results) {
 
-			GeoResult<T> geoResult = callback.doWith(element);
+			GeoResult<R> geoResult = callback.doWith(element);
 			aggregate = aggregate.add(BigDecimal.valueOf(geoResult.getDistance().getValue()));
 			result.add(geoResult);
 		}
 
-		Distance avgDistance = new Distance(
+		Distance avgDistance = Distance.of(
 				result.size() == 0 ? 0 : aggregate.divide(new BigDecimal(result.size()), RoundingMode.HALF_UP).doubleValue(),
 				near.getMetric());
 
 		return new GeoResults<>(result, avgDistance);
 	}
 
-	@Nullable
-	@Override
-	public <T> T findAndModify(Query query, UpdateDefinition update, Class<T> entityClass) {
+	public <T> @Nullable T findAndModify(Query query, UpdateDefinition update, Class<T> entityClass) {
 		return findAndModify(query, update, new FindAndModifyOptions(), entityClass, getCollectionName(entityClass));
 	}
 
-	@Nullable
 	@Override
-	public <T> T findAndModify(Query query, UpdateDefinition update, Class<T> entityClass, String collectionName) {
+	public <T> @Nullable T findAndModify(Query query, UpdateDefinition update, Class<T> entityClass,
+			String collectionName) {
 		return findAndModify(query, update, new FindAndModifyOptions(), entityClass, collectionName);
 	}
 
-	@Nullable
 	@Override
-	public <T> T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class<T> entityClass) {
+	public <T> @Nullable T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options,
+			Class<T> entityClass) {
 		return findAndModify(query, update, options, entityClass, getCollectionName(entityClass));
 	}
 
-	@Nullable
 	@Override
-	public <T> T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class<T> entityClass,
-			String collectionName) {
+	public <T> @Nullable T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options,
+		Class<T> entityClass, String collectionName) {
+		return findAndModify(query, update, options, entityClass, collectionName, QueryResultConverter.entity());
+	}
+
+	<S, T> @Nullable T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options,
+			Class<S> entityClass, String collectionName, QueryResultConverter<? super S, ? extends T> resultConverter) {
 
 		Assert.notNull(query, "Query must not be null");
 		Assert.notNull(update, "Update must not be null");
@@ -1108,12 +1129,17 @@ public <T> T findAndModify(Query query, UpdateDefinition update, FindAndModifyOp
 		}
 
 		return doFindAndModify(createDelegate(query), collectionName, query.getQueryObject(), query.getFieldsObject(),
-				getMappedSortObject(query, entityClass), entityClass, update, optionsToUse);
+				getMappedSortObject(query, entityClass), entityClass, update, optionsToUse, resultConverter);
 	}
 
 	@Override
-	public <S, T> T findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class<S> entityType,
-			String collectionName, Class<T> resultType) {
+	public <S, T> @Nullable T findAndReplace(Query query, S replacement, FindAndReplaceOptions options,
+		Class<S> entityType, String collectionName, Class<T> resultType) {
+		return findAndReplace(query, replacement, options, entityType, collectionName, resultType, QueryResultConverter.entity());
+	}
+
+	<S, T, R> @Nullable R findAndReplace(Query query, S replacement, FindAndReplaceOptions options,
+			Class<S> entityType, String collectionName, Class<T> resultType, QueryResultConverter<? super T, ? extends R> resultConverter) {
 
 		Assert.notNull(query, "Query must not be null");
 		Assert.notNull(replacement, "Replacement must not be null");
@@ -1140,8 +1166,8 @@ public <S, T> T findAndReplace(Query query, S replacement, FindAndReplaceOptions
 		maybeEmitEvent(new BeforeSaveEvent<>(replacement, mappedReplacement, collectionName));
 		maybeCallBeforeSave(replacement, mappedReplacement, collectionName);
 
-		T saved = doFindAndReplace(collectionPreparer, collectionName, mappedQuery, mappedFields, mappedSort,
-				queryContext.getCollation(entityType).orElse(null), entityType, mappedReplacement, options, projection);
+		R saved = doFindAndReplace(collectionPreparer, collectionName, mappedQuery, mappedFields, mappedSort,
+				queryContext.getCollation(entityType).orElse(null), entityType, mappedReplacement, options, projection, resultConverter);
 
 		if (saved != null) {
 			maybeEmitEvent(new AfterSaveEvent<>(saved, mappedReplacement, collectionName));
@@ -1154,15 +1180,13 @@ public <S, T> T findAndReplace(Query query, S replacement, FindAndReplaceOptions
 	// Find methods that take a Query to express the query and that return a single object that is also removed from the
 	// collection in the database.
 
-	@Nullable
 	@Override
-	public <T> T findAndRemove(Query query, Class<T> entityClass) {
+	public <T> @Nullable T findAndRemove(Query query, Class<T> entityClass) {
 		return findAndRemove(query, entityClass, getCollectionName(entityClass));
 	}
 
-	@Nullable
 	@Override
-	public <T> T findAndRemove(Query query, Class<T> entityClass, String collectionName) {
+	public <T> @Nullable T findAndRemove(Query query, Class<T> entityClass, String collectionName) {
 
 		Assert.notNull(query, "Query must not be null");
 		Assert.notNull(entityClass, "EntityClass must not be null");
@@ -1224,6 +1248,7 @@ public long estimatedCount(String collectionName) {
 		return doEstimatedCount(CollectionPreparerDelegate.of(this), collectionName, new EstimatedDocumentCountOptions());
 	}
 
+	@SuppressWarnings("NullAway")
 	protected long doEstimatedCount(CollectionPreparer<MongoCollection<Document>> collectionPreparer,
 			String collectionName, EstimatedDocumentCountOptions options) {
 		return execute(collectionName,
@@ -1241,6 +1266,7 @@ public long exactCount(Query query, @Nullable Class<?> entityClass, String colle
 		return doExactCount(createDelegate(query), collectionName, mappedQuery, options);
 	}
 
+	@SuppressWarnings("NullAway")
 	protected long doExactCount(CollectionPreparer<MongoCollection<Document>> collectionPreparer, String collectionName,
 			Document filter, CountOptions options) {
 		return execute(collectionName, collection -> collectionPreparer.prepare(collection)
@@ -1433,7 +1459,10 @@ protected <T> Collection<T> doInsertBatch(String collectionName, Collection<? ex
 			maybeEmitEvent(new BeforeSaveEvent<>(initialized, document, collectionName));
 			initialized = maybeCallBeforeSave(initialized, document, collectionName);
 
-			documentList.add(document);
+			MappedDocument mappedDocument = queryOperations.createInsertContext(MappedDocument.of(document))
+				.prepareId(uninitialized.getClass());
+
+			documentList.add(mappedDocument.getDocument());
 			initializedBatchToSave.add(initialized);
 		}
 
@@ -1541,7 +1570,7 @@ protected <T> T doSave(String collectionName, T objectToSave, MongoWriter<T> wri
 		return maybeCallAfterSave(saved, dbDoc, collectionName);
 	}
 
-	@SuppressWarnings("ConstantConditions")
+	@SuppressWarnings({ "ConstantConditions", "NullAway" })
 	protected Object insertDocument(String collectionName, Document document, Class<?> entityClass) {
 
 		if (LOGGER.isDebugEnabled()) {
@@ -1595,6 +1624,7 @@ protected List<Object> insertDocumentList(String collectionName, List<Document>
 		return MappedDocument.toIds(documents);
 	}
 
+	@SuppressWarnings("NullAway")
 	protected Object saveDocument(String collectionName, Document dbDoc, Class<?> entityClass) {
 
 		if (LOGGER.isDebugEnabled()) {
@@ -1693,7 +1723,7 @@ public UpdateResult updateMulti(Query query, UpdateDefinition update, Class<?> e
 		return doUpdate(collectionName, query, update, entityClass, false, true);
 	}
 
-	@SuppressWarnings("ConstantConditions")
+	@SuppressWarnings({ "ConstantConditions", "NullAway" })
 	protected UpdateResult doUpdate(String collectionName, Query query, UpdateDefinition update,
 			@Nullable Class<?> entityClass, boolean upsert, boolean multi) {
 
@@ -1803,7 +1833,7 @@ public DeleteResult remove(Query query, Class<?> entityClass, String collectionN
 		return doRemove(collectionName, query, entityClass, true);
 	}
 
-	@SuppressWarnings("ConstantConditions")
+	@SuppressWarnings({ "ConstantConditions", "NullAway" })
 	protected <T> DeleteResult doRemove(String collectionName, Query query, @Nullable Class<T> entityClass,
 			boolean multi) {
 
@@ -1977,11 +2007,6 @@ public <T> List<T> mapReduce(Query query, Class<?> domainType, String inputColle
 				mapReduce = mapReduce.jsMode(mapReduceOptions.getJavaScriptMode());
 			}
 
-			if (mapReduceOptions.getOutputSharded().isPresent()) {
-				MongoCompatibilityAdapter.mapReduceIterableAdapter(mapReduce)
-						.sharded(mapReduceOptions.getOutputSharded().get());
-			}
-
 			if (StringUtils.hasText(mapReduceOptions.getOutputCollection()) && !mapReduceOptions.usesInlineOutput()) {
 
 				mapReduce = mapReduce.collectionName(mapReduceOptions.getOutputCollection())
@@ -2019,7 +2044,7 @@ public <O> AggregationResults<O> aggregate(TypedAggregation<?> aggregation, Clas
 	@Override
 	public <O> AggregationResults<O> aggregate(TypedAggregation<?> aggregation, String inputCollectionName,
 			Class<O> outputType) {
-		return aggregate(aggregation, inputCollectionName, outputType, null);
+		return aggregate(aggregation, inputCollectionName, outputType, (AggregationOperationContext) null);
 	}
 
 	@Override
@@ -2032,7 +2057,7 @@ public <O> AggregationResults<O> aggregate(Aggregation aggregation, Class<?> inp
 
 	@Override
 	public <O> AggregationResults<O> aggregate(Aggregation aggregation, String collectionName, Class<O> outputType) {
-		return aggregate(aggregation, collectionName, outputType, null);
+		return doAggregate(aggregation, collectionName, outputType, QueryResultConverter.entity());
 	}
 
 	@Override
@@ -2130,17 +2155,39 @@ protected <S, T> UpdateResult replace(Query query, Class<S> entityType, T replac
 	 * @return
 	 */
 	protected <T> List<T> doFindAndDelete(String collectionName, Query query, Class<T> entityClass) {
+		return doFindAndDelete(collectionName, query, entityClass, QueryResultConverter.entity());
+	}
 
-		List<T> result = find(query, entityClass, collectionName);
+	@SuppressWarnings("NullAway")
+	<S, T> List<T> doFindAndDelete(String collectionName, Query query, Class<S> entityClass,
+			QueryResultConverter<? super S, ? extends T> resultConverter) {
+
+		List<Object> ids = new ArrayList<>();
+
+		QueryResultConverterCallback<S, T> callback = new QueryResultConverterCallback<>(resultConverter,
+				new ProjectingReadCallback<>(getConverter(), EntityProjection.nonProjecting(entityClass), collectionName)) {
+			@Override
+			public T doWith(Document object) {
+				ids.add(object.get("_id"));
+				return super.doWith(object);
+			}
+		};
+
+		List<T> result = doFind(collectionName, createDelegate(query), query.getQueryObject(), query.getFieldsObject(), entityClass,
+			new QueryCursorPreparer(query, entityClass), callback);
 
 		if (!CollectionUtils.isEmpty(result)) {
 
-			Query byIdInQuery = operations.getByIdInQuery(result);
+			Criteria[] criterias = ids.stream() //
+				.map(it -> Criteria.where("_id").is(it)) //
+				.toArray(Criteria[]::new);
+
+			Query removeQuery = new Query(criterias.length == 1 ? criterias[0] : new Criteria().orOperator(criterias));
 			if (query.hasReadPreference()) {
-				byIdInQuery.withReadPreference(query.getReadPreference());
+				removeQuery.withReadPreference(query.getReadPreference());
 			}
 
-			remove(byIdInQuery, entityClass, collectionName);
+			remove(removeQuery, entityClass, collectionName);
 		}
 
 		return result;
@@ -2162,11 +2209,25 @@ private <O> AggregationResults<O> doAggregate(Aggregation aggregation, String co
 		return doAggregate(aggregation, collectionName, outputType, context.getAggregationOperationContext());
 	}
 
-	@SuppressWarnings("ConstantConditions")
+	<T, O> AggregationResults<O> doAggregate(Aggregation aggregation, String collectionName, Class<T> outputType,
+			QueryResultConverter<? super T, ? extends O> resultConverter) {
+
+		return doAggregate(aggregation, collectionName, outputType, resultConverter, queryOperations
+				.createAggregation(aggregation, (AggregationOperationContext) null).getAggregationOperationContext());
+	}
+
+	@SuppressWarnings({ "ConstantConditions", "NullAway" })
 	protected <O> AggregationResults<O> doAggregate(Aggregation aggregation, String collectionName, Class<O> outputType,
 			AggregationOperationContext context) {
+		return doAggregate(aggregation, collectionName, outputType, QueryResultConverter.entity(), context);
+	}
+
+	@SuppressWarnings({"ConstantConditions", "NullAway"})
+	<T, O> AggregationResults<O> doAggregate(Aggregation aggregation, String collectionName, Class<T> outputType,
+			QueryResultConverter<? super T, ? extends O> resultConverter, AggregationOperationContext context) {
 
-		ReadDocumentCallback<O> callback = new ReadDocumentCallback<>(mongoConverter, outputType, collectionName);
+		DocumentCallback<O> callback = new QueryResultConverterCallback<>(resultConverter,
+				new ReadDocumentCallback<>(mongoConverter, outputType, collectionName));
 
 		AggregationOptions options = aggregation.getOptions();
 		AggregationUtil aggregationUtil = new AggregationUtil(queryMapper, mappingContext);
@@ -2245,9 +2306,15 @@ protected <O> AggregationResults<O> doAggregate(Aggregation aggregation, String
 		});
 	}
 
-	@SuppressWarnings("ConstantConditions")
 	protected <O> Stream<O> aggregateStream(Aggregation aggregation, String collectionName, Class<O> outputType,
 			@Nullable AggregationOperationContext context) {
+		return doAggregateStream(aggregation, collectionName, outputType, QueryResultConverter.entity(), context);
+	}
+
+	@SuppressWarnings({ "ConstantConditions", "NullAway" })
+	<T, O> Stream<O> doAggregateStream(Aggregation aggregation, String collectionName, Class<T> outputType,
+			QueryResultConverter<? super T, ? extends O> resultConverter,
+			@Nullable AggregationOperationContext context) {
 
 		Assert.notNull(aggregation, "Aggregation pipeline must not be null");
 		Assert.hasText(collectionName, "Collection name must not be null or empty");
@@ -2264,7 +2331,8 @@ protected <O> Stream<O> aggregateStream(Aggregation aggregation, String collecti
 					String.format("Streaming aggregation: %s in collection %s", serializeToJsonSafely(pipeline), collectionName));
 		}
 
-		ReadDocumentCallback<O> readCallback = new ReadDocumentCallback<>(mongoConverter, outputType, collectionName);
+		DocumentCallback<O> readCallback = new QueryResultConverterCallback<>(resultConverter,
+				new ReadDocumentCallback<>(mongoConverter, outputType, collectionName));
 
 		return execute(collectionName, (CollectionCallback<Stream<O>>) collection -> {
 
@@ -2290,7 +2358,8 @@ protected <O> Stream<O> aggregateStream(Aggregation aggregation, String collecti
 				cursor = cursor.maxTime(options.getMaxTime().toMillis(), TimeUnit.MILLISECONDS);
 			}
 
-			Class<?> domainType = aggregation instanceof TypedAggregation typedAggregation ? typedAggregation.getInputType()
+			Class<?> domainType = aggregation instanceof TypedAggregation<?> typedAggregation
+					? typedAggregation.getInputType()
 					: null;
 
 			Optionals.firstNonEmpty(options::getCollation, //
@@ -2360,11 +2429,11 @@ protected String replaceWithResourceIfNecessary(String function) {
 	}
 
 	@Override
-	@SuppressWarnings("ConstantConditions")
+	@SuppressWarnings({ "ConstantConditions", "NullAway" })
 	public Set<String> getCollectionNames() {
 		return execute(db -> {
 			Set<String> result = new LinkedHashSet<>();
-			for (String name : MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(db).listCollectionNames()) {
+			for (String name : db.listCollectionNames()) {
 				result.add(name);
 			}
 			return result;
@@ -2444,7 +2513,7 @@ protected MongoCollection<Document> doCreateCollection(String collectionName, Do
 	 * @return the collection that was created
 	 * @since 3.3.3
 	 */
-	@SuppressWarnings("ConstantConditions")
+	@SuppressWarnings({ "ConstantConditions", "NullAway" })
 	protected MongoCollection<Document> doCreateCollection(String collectionName,
 			CreateCollectionOptions collectionOptions) {
 
@@ -2523,8 +2592,9 @@ private CreateCollectionOptions getCreateCollectionOptions(Document document) {
 	 * @return the converted object or {@literal null} if none exists.
 	 */
 	@Nullable
-	protected <T> T doFindOne(String collectionName, CollectionPreparer<MongoCollection<Document>> collectionPreparer,
-			Document query, Document fields, Class<T> entityClass) {
+	protected <T> T doFindOne(String collectionName,
+			CollectionPreparer<MongoCollection<Document>> collectionPreparer, Document query, Document fields,
+			Class<T> entityClass) {
 		return doFindOne(collectionName, collectionPreparer, query, fields, CursorPreparer.NO_OP_PREPARER, entityClass);
 	}
 
@@ -2543,8 +2613,9 @@ protected <T> T doFindOne(String collectionName, CollectionPreparer<MongoCollect
 	 */
 	@Nullable
 	@SuppressWarnings("ConstantConditions")
-	protected <T> T doFindOne(String collectionName, CollectionPreparer<MongoCollection<Document>> collectionPreparer,
-			Document query, Document fields, CursorPreparer preparer, Class<T> entityClass) {
+	protected <T> T doFindOne(String collectionName,
+			CollectionPreparer<MongoCollection<Document>> collectionPreparer, Document query, Document fields,
+			CursorPreparer preparer, Class<T> entityClass) {
 
 		MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(entityClass);
 
@@ -2610,7 +2681,9 @@ protected <S, T> List<T> doFind(String collectionName,
 
 		if (LOGGER.isDebugEnabled()) {
 
-			Document mappedSort = preparer instanceof SortingQueryCursorPreparer sqcp ?  getMappedSortObject(sqcp.getSortObject(), entity) : null;
+			Document mappedSort = preparer instanceof SortingQueryCursorPreparer sqcp
+					? getMappedSortObject(sqcp.getSortObject(), entity)
+					: null;
 			LOGGER.debug(String.format("find using query: %s fields: %s sort: %s for class: %s in collection: %s",
 					serializeToJsonSafely(mappedQuery), mappedFields, serializeToJsonSafely(mappedSort), entityClass,
 					collectionName));
@@ -2626,11 +2699,12 @@ protected <S, T> List<T> doFind(String collectionName,
 	 *
 	 * @since 2.0
 	 */
-	<S, T> List<T> doFind(CollectionPreparer<MongoCollection<Document>> collectionPreparer, String collectionName,
-			Document query, Document fields, Class<S> sourceClass, Class<T> targetClass, CursorPreparer preparer) {
+	<T, R> List<R> doFind(CollectionPreparer<MongoCollection<Document>> collectionPreparer, String collectionName,
+			Document query, Document fields, Class<?> sourceClass, Class<T> targetClass,
+			QueryResultConverter<? super T, ? extends R> resultConverter, CursorPreparer preparer) {
 
 		MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(sourceClass);
-		EntityProjection<T, S> projection = operations.introspectProjection(targetClass, sourceClass);
+		EntityProjection<T, ?> projection = operations.introspectProjection(targetClass, sourceClass);
 
 		QueryContext queryContext = queryOperations.createQueryContext(new BasicQuery(query, fields));
 		Document mappedFields = queryContext.getMappedFields(entity, projection);
@@ -2646,8 +2720,9 @@ <S, T> List<T> doFind(CollectionPreparer<MongoCollection<Document>> collectionPr
 					collectionName));
 		}
 
+		DocumentCallback<R> callback = getResultReader(projection, collectionName, resultConverter);
 		return executeFindMultiInternal(new FindCallback(collectionPreparer, mappedQuery, mappedFields, null), preparer,
-				new ProjectingReadCallback<>(mongoConverter, projection, collectionName), collectionName);
+				callback, collectionName);
 	}
 
 	/**
@@ -2720,8 +2795,9 @@ Document getMappedValidator(Validator validator, Class<?> domainType) {
 	 * @return the List of converted objects.
 	 */
 	@SuppressWarnings("ConstantConditions")
-	protected <T> T doFindAndRemove(CollectionPreparer collectionPreparer, String collectionName, Document query,
-			Document fields, Document sort, @Nullable Collation collation, Class<T> entityClass) {
+	protected <T> @Nullable T doFindAndRemove(CollectionPreparer collectionPreparer, String collectionName,
+			Document query, @Nullable Document fields, @Nullable Document sort, @Nullable Collation collation,
+			Class<T> entityClass) {
 
 		if (LOGGER.isDebugEnabled()) {
 			LOGGER.debug(String.format("findAndRemove using query: %s fields: %s sort: %s for class: %s in collection: %s",
@@ -2736,9 +2812,10 @@ protected <T> T doFindAndRemove(CollectionPreparer collectionPreparer, String co
 	}
 
 	@SuppressWarnings("ConstantConditions")
-	protected <T> T doFindAndModify(CollectionPreparer collectionPreparer, String collectionName, Document query,
-			Document fields, Document sort, Class<T> entityClass, UpdateDefinition update,
-			@Nullable FindAndModifyOptions options) {
+	<S, T> @Nullable T doFindAndModify(CollectionPreparer<MongoCollection<Document>> collectionPreparer,
+			String collectionName,
+			Document query, @Nullable Document fields, @Nullable Document sort, Class<S> entityClass, UpdateDefinition update,
+			@Nullable FindAndModifyOptions options, QueryResultConverter<? super S, ? extends T> resultConverter) {
 
 		if (options == null) {
 			options = new FindAndModifyOptions();
@@ -2760,10 +2837,12 @@ protected <T> T doFindAndModify(CollectionPreparer collectionPreparer, String co
 					serializeToJsonSafely(mappedUpdate), collectionName));
 		}
 
+		DocumentCallback<T> callback = getResultReader(EntityProjection.nonProjecting(entityClass), collectionName, resultConverter);
+
 		return executeFindOneInternal(
 				new FindAndModifyCallback(collectionPreparer, mappedQuery, fields, sort, mappedUpdate,
 						update.getArrayFilters().stream().map(ArrayFilter::asDocument).collect(Collectors.toList()), options),
-				new ReadDocumentCallback<>(this.mongoConverter, entityClass, collectionName), collectionName);
+				callback, collectionName);
 	}
 
 	/**
@@ -2782,14 +2861,16 @@ protected <T> T doFindAndModify(CollectionPreparer collectionPreparer, String co
 	 *         {@literal false} and {@link FindAndReplaceOptions#isUpsert() upsert} is {@literal false}.
 	 */
 	@Nullable
-	protected <T> T doFindAndReplace(CollectionPreparer collectionPreparer, String collectionName, Document mappedQuery,
-			Document mappedFields, Document mappedSort, @Nullable com.mongodb.client.model.Collation collation,
-			Class<?> entityType, Document replacement, FindAndReplaceOptions options, Class<T> resultType) {
+	protected <S, T> T doFindAndReplace(CollectionPreparer<MongoCollection<Document>> collectionPreparer,
+			String collectionName,
+			Document mappedQuery, Document mappedFields, Document mappedSort,
+			com.mongodb.client.model.@Nullable Collation collation, Class<S> entityType, Document replacement,
+			FindAndReplaceOptions options, Class<T> resultType) {
 
-		EntityProjection<T, ?> projection = operations.introspectProjection(resultType, entityType);
+		EntityProjection<T, S> projection = operations.introspectProjection(resultType, entityType);
 
 		return doFindAndReplace(collectionPreparer, collectionName, mappedQuery, mappedFields, mappedSort, collation,
-				entityType, replacement, options, projection);
+				entityType, replacement, options, projection, QueryResultConverter.entity());
 	}
 
 	CollectionPreparerDelegate createDelegate(Query query) {
@@ -2824,9 +2905,11 @@ CollectionPreparer<MongoCollection<Document>> createCollectionPreparer(Query que
 	 * @since 3.4
 	 */
 	@Nullable
-	private <T> T doFindAndReplace(CollectionPreparer collectionPreparer, String collectionName, Document mappedQuery,
-			Document mappedFields, Document mappedSort, @Nullable com.mongodb.client.model.Collation collation,
-			Class<?> entityType, Document replacement, FindAndReplaceOptions options, EntityProjection<T, ?> projection) {
+	private <S, T, R> R doFindAndReplace(CollectionPreparer<MongoCollection<Document>> collectionPreparer,
+			String collectionName,
+			Document mappedQuery, Document mappedFields, Document mappedSort,
+			com.mongodb.client.model.@Nullable Collation collation, Class<T> entityType, Document replacement,
+			FindAndReplaceOptions options, EntityProjection<S, T> projection, QueryResultConverter<? super S, ? extends R> resultConverter) {
 
 		if (LOGGER.isDebugEnabled()) {
 			LOGGER
@@ -2837,11 +2920,13 @@ private <T> T doFindAndReplace(CollectionPreparer collectionPreparer, String col
 							serializeToJsonSafely(mappedSort), entityType, serializeToJsonSafely(replacement), collectionName));
 		}
 
+		DocumentCallback<R> callback = getResultReader(projection, collectionName, resultConverter);
 		return executeFindOneInternal(new FindAndReplaceCallback(collectionPreparer, mappedQuery, mappedFields, mappedSort,
-				replacement, collation, options), new ProjectingReadCallback<>(mongoConverter, projection, collectionName),
+				replacement, collation, options),callback,
 				collectionName);
 	}
 
+	@SuppressWarnings("NullAway")
 	private <S> UpdateResult doReplace(ReplaceOptions options, Class<S> entityType, String collectionName,
 			UpdateContext updateContext, CollectionPreparer<MongoCollection<Document>> collectionPreparer,
 			Document replacement) {
@@ -2966,6 +3051,16 @@ private void executeQueryInternal(CollectionCallback<FindIterable<Document>> col
 		}
 	}
 
+	@SuppressWarnings("unchecked")
+	private <T, R> DocumentCallback<R> getResultReader(EntityProjection<T, ?> projection, String collectionName,
+			QueryResultConverter<? super T, ? extends R> resultConverter) {
+
+		DocumentCallback<T> readCallback = new ProjectingReadCallback<>(mongoConverter, projection, collectionName);
+
+		return resultConverter == QueryResultConverter.entity() ? (DocumentCallback<R>) readCallback
+				: new QueryResultConverterCallback<T, R>(resultConverter, readCallback);
+	}
+
 	public PersistenceExceptionTranslator getExceptionTranslator() {
 		return exceptionTranslator;
 	}
@@ -3002,13 +3097,12 @@ private Document getMappedSortObject(@Nullable Query query, Class<?> type) {
 		return getMappedSortObject(query.getSortObject(), type);
 	}
 
-	@Nullable
-	private Document getMappedSortObject(Document sortObject, Class<?> type) {
+	private @Nullable Document getMappedSortObject(@Nullable Document sortObject, Class<?> type) {
 		return getMappedSortObject(sortObject, mappingContext.getPersistentEntity(type));
 	}
 
-	@Nullable
-	private Document getMappedSortObject(Document sortObject, @Nullable MongoPersistentEntity<?> entity) {
+
+	private @Nullable Document getMappedSortObject(@Nullable Document sortObject, @Nullable MongoPersistentEntity<?> entity) {
 
 		if (ObjectUtils.isEmpty(sortObject)) {
 			return null;
@@ -3084,10 +3178,10 @@ private static class FindCallback implements CollectionCallback<FindIterable<Doc
 		private final CollectionPreparer<MongoCollection<Document>> collectionPreparer;
 		private final Document query;
 		private final Document fields;
-		private final @Nullable com.mongodb.client.model.Collation collation;
+		private final com.mongodb.client.model.@Nullable Collation collation;
 
 		public FindCallback(CollectionPreparer<MongoCollection<Document>> collectionPreparer, Document query,
-				Document fields, @Nullable com.mongodb.client.model.Collation collation) {
+				Document fields, com.mongodb.client.model.@Nullable Collation collation) {
 
 			Assert.notNull(query, "Query must not be null");
 			Assert.notNull(fields, "Fields must not be null");
@@ -3123,10 +3217,10 @@ private class ExistsCallback implements CollectionCallback<Boolean> {
 
 		private final CollectionPreparer collectionPreparer;
 		private final Document mappedQuery;
-		private final com.mongodb.client.model.Collation collation;
+		private final com.mongodb.client.model.@Nullable Collation collation;
 
 		ExistsCallback(CollectionPreparer collectionPreparer, Document mappedQuery,
-				com.mongodb.client.model.Collation collation) {
+				com.mongodb.client.model.@Nullable Collation collation) {
 
 			this.collectionPreparer = collectionPreparer;
 			this.mappedQuery = mappedQuery;
@@ -3151,12 +3245,12 @@ private static class FindAndRemoveCallback implements CollectionCallback<Documen
 
 		private final CollectionPreparer<MongoCollection<Document>> collectionPreparer;
 		private final Document query;
-		private final Document fields;
-		private final Document sort;
+		private final @Nullable Document fields;
+		private final @Nullable Document sort;
 		private final Optional<Collation> collation;
 
 		FindAndRemoveCallback(CollectionPreparer<MongoCollection<Document>> collectionPreparer, Document query,
-				Document fields, Document sort, @Nullable Collation collation) {
+				@Nullable Document fields, @Nullable Document sort, @Nullable Collation collation) {
 			this.collectionPreparer = collectionPreparer;
 
 			this.query = query;
@@ -3179,14 +3273,15 @@ private static class FindAndModifyCallback implements CollectionCallback<Documen
 
 		private final CollectionPreparer<MongoCollection<Document>> collectionPreparer;
 		private final Document query;
-		private final Document fields;
-		private final Document sort;
+		private final @Nullable Document fields;
+		private final @Nullable Document sort;
 		private final Object update;
 		private final List<Document> arrayFilters;
 		private final FindAndModifyOptions options;
 
 		FindAndModifyCallback(CollectionPreparer<MongoCollection<Document>> collectionPreparer, Document query,
-				Document fields, Document sort, Object update, List<Document> arrayFilters, FindAndModifyOptions options) {
+				@Nullable Document fields, @Nullable Document sort, Object update, List<Document> arrayFilters,
+				FindAndModifyOptions options) {
 
 			this.collectionPreparer = collectionPreparer;
 			this.query = query;
@@ -3240,11 +3335,11 @@ private static class FindAndReplaceCallback implements CollectionCallback<Docume
 		private final Document fields;
 		private final Document sort;
 		private final Document update;
-		private final @Nullable com.mongodb.client.model.Collation collation;
+		private final  com.mongodb.client.model.@Nullable Collation collation;
 		private final FindAndReplaceOptions options;
 
 		FindAndReplaceCallback(CollectionPreparer<MongoCollection<Document>> collectionPreparer, Document query,
-				Document fields, Document sort, Document update, @Nullable com.mongodb.client.model.Collation collation,
+				Document fields, Document sort, Document update, com.mongodb.client.model.@Nullable Collation collation,
 				FindAndReplaceOptions options) {
 			this.collectionPreparer = collectionPreparer;
 			this.query = query;
@@ -3325,6 +3420,24 @@ public T doWith(Document document) {
 		}
 	}
 
+	static class QueryResultConverterCallback<T, R> implements DocumentCallback<R> {
+
+		private final QueryResultConverter<? super T, ? extends R> converter;
+		private final DocumentCallback<T> delegate;
+
+		QueryResultConverterCallback(QueryResultConverter<? super T, ? extends R> converter, DocumentCallback<T> delegate) {
+			this.converter = converter;
+			this.delegate = delegate;
+		}
+
+		@Override
+		public R doWith(Document object) {
+
+			Lazy<T> lazy = Lazy.of(() -> delegate.doWith(object));
+			return converter.mapDocument(object, lazy::get);
+		}
+	}
+
 	/**
 	 * {@link DocumentCallback} transforming {@link Document} into the given {@code targetType} or decorating the
 	 * {@code sourceType} with a {@literal projection} in case the {@code targetType} is an {@literal interface}.
@@ -3350,10 +3463,6 @@ private class ProjectingReadCallback<S, T> implements DocumentCallback<T> {
 		@SuppressWarnings("unchecked")
 		public T doWith(Document document) {
 
-			if (document == null) {
-				return null;
-			}
-
 			maybeEmitEvent(new AfterLoadEvent<>(document, projection.getMappedType().getType(), collectionName));
 
 			Object entity = mongoConverter.project(projection, document);
@@ -3509,7 +3618,7 @@ public GeoResult<T> doWith(Document object) {
 
 			T doWith = delegate.doWith(object);
 
-			return new GeoResult<>(doWith, new Distance(distance, metric));
+			return new GeoResult<>(doWith, Distance.of(distance, metric));
 		}
 	}
 
@@ -3598,8 +3707,6 @@ public void close() {
 				throw potentiallyConvertRuntimeException(ex, exceptionTranslator);
 			} finally {
 				cursor = null;
-				exceptionTranslator = null;
-				objectReadCallback = null;
 			}
 		}
 	}
@@ -3631,7 +3738,7 @@ static class SessionBoundMongoTemplate extends MongoTemplate {
 		}
 
 		@Override
-		public MongoCollection<Document> getCollection(String collectionName) {
+		public MongoCollection<Document> getCollection(@Nullable String collectionName) {
 
 			// native MongoDB objects that offer methods with ClientSession must not be proxied.
 			return delegate.getCollection(collectionName);
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java
index 28ca85fbd7..4ae618eaa1 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java
@@ -31,6 +31,7 @@
 import org.bson.codecs.Codec;
 import org.bson.conversions.Bson;
 import org.bson.types.ObjectId;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mapping.PropertyPath;
 import org.springframework.data.mapping.PropertyReferenceException;
 import org.springframework.data.mapping.context.MappingContext;
@@ -62,7 +63,7 @@
 import org.springframework.data.mongodb.util.BsonUtils;
 import org.springframework.data.projection.EntityProjection;
 import org.springframework.data.util.Lazy;
-import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 
 import com.mongodb.client.model.CountOptions;
@@ -283,6 +284,7 @@ <T> MappedDocument prepareId(Class<T> type) {
 		 * @param <T>
 		 * @return the {@link MappedDocument} containing the changes.
 		 */
+		@SuppressWarnings("NullAway")
 		<T> MappedDocument prepareId(@Nullable MongoPersistentEntity<T> entity) {
 
 			if (entity == null || source.hasId()) {
@@ -361,6 +363,7 @@ <T> Document getMappedQuery(@Nullable MongoPersistentEntity<T> entity) {
 			return queryMapper.getMappedObject(getQueryObject(), entity);
 		}
 
+		@SuppressWarnings("NullAway")
 		Document getMappedFields(@Nullable MongoPersistentEntity<?> entity, EntityProjection<?, ?> projection) {
 
 			Document fields = evaluateFields(entity);
@@ -888,6 +891,8 @@ Document getMappedShardKey(MongoPersistentEntity<?> entity) {
 		 */
 		List<Document> getUpdatePipeline(@Nullable Class<?> domainType) {
 
+			Assert.isInstanceOf(AggregationUpdate.class, update);
+
 			Class<?> type = domainType != null ? domainType : Object.class;
 
 			AggregationOperationContext context = new RelaxedTypeBasedAggregationOperationContext(type, mappingContext,
@@ -901,6 +906,7 @@ List<Document> getUpdatePipeline(@Nullable Class<?> domainType) {
 		 * @param entity
 		 * @return
 		 */
+		@SuppressWarnings("NullAway")
 		Document getMappedUpdate(@Nullable MongoPersistentEntity<?> entity) {
 
 			if (update != null) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryResultConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryResultConverter.java
new file mode 100644
index 0000000000..ca93940a9c
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryResultConverter.java
@@ -0,0 +1,85 @@
+/*
+ * 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.mongodb.core;
+
+import org.bson.Document;
+
+/**
+ * Converter for MongoDB query results.
+ * <p>
+ * This is a functional interface that allows for mapping a {@link Document} to a result type.
+ * {@link #mapDocument(Document, ConversionResultSupplier) row mapping} can obtain upstream a
+ * {@link ConversionResultSupplier upstream converter} to enrich the final result object. This is useful when e.g.
+ * wrapping result objects where the wrapper needs to obtain information from the actual {@link Document}.
+ *
+ * @param <T> object type accepted by this converter.
+ * @param <R> the returned result type.
+ * @author Mark Paluch
+ * @since 5.0
+ */
+@FunctionalInterface
+public interface QueryResultConverter<T, R> {
+
+	/**
+	 * Returns a function that returns the materialized entity.
+	 *
+	 * @param <T> the type of the input and output entity to the function.
+	 * @return a function that returns the materialized entity.
+	 */
+	@SuppressWarnings("unchecked")
+	static <T> QueryResultConverter<T, T> entity() {
+		return (QueryResultConverter<T, T>) EntityResultConverter.INSTANCE;
+	}
+
+	/**
+	 * Map a {@link Document} that is read from the MongoDB query/aggregation operation to a query result.
+	 *
+	 * @param document the raw document from the MongoDB query/aggregation result.
+	 * @param reader reader object that supplies an upstream result from an earlier converter.
+	 * @return the mapped result.
+	 */
+	R mapDocument(Document document, ConversionResultSupplier<T> reader);
+
+	/**
+	 * Returns a composed function that first applies this function to its input, and then applies the {@code after}
+	 * function to the result. If evaluation of either function throws an exception, it is relayed to the caller of the
+	 * composed function.
+	 *
+	 * @param <V> the type of output of the {@code after} function, and of the composed function.
+	 * @param after the function to apply after this function is applied.
+	 * @return a composed function that first applies this function and then applies the {@code after} function.
+	 */
+	default <V> QueryResultConverter<T, V> andThen(QueryResultConverter<? super R, ? extends V> after) {
+		return (row, reader) -> after.mapDocument(row, () -> mapDocument(row, reader));
+	}
+
+	/**
+	 * A supplier that converts a {@link Document} into {@code T}. Allows for lazy reading of query results.
+	 *
+	 * @param <T> type of the returned result.
+	 */
+	interface ConversionResultSupplier<T> {
+
+		/**
+		 * Obtain the upstream conversion result.
+		 *
+		 * @return the upstream conversion result.
+		 */
+		T get();
+
+	}
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperation.java
index 54129e6b5d..99c94b19e4 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperation.java
@@ -18,6 +18,7 @@
 import reactor.core.publisher.Flux;
 
 import org.springframework.data.mongodb.core.aggregation.Aggregation;
+import org.springframework.lang.Contract;
 
 /**
  * {@link ReactiveAggregationOperation} allows creation and execution of reactive MongoDB aggregation operations in a
@@ -44,7 +45,7 @@ public interface ReactiveAggregationOperation {
 	/**
 	 * Start creating an aggregation operation that returns results mapped to the given domain type. <br />
 	 * Use {@link org.springframework.data.mongodb.core.aggregation.TypedAggregation} to specify a potentially different
-	 * input type for he aggregation.
+	 * input type for the aggregation.
 	 *
 	 * @param domainType must not be {@literal null}.
 	 * @return new instance of {@link ReactiveAggregation}. Never {@literal null}.
@@ -73,6 +74,18 @@ interface AggregationOperationWithCollection<T> {
 	 */
 	interface TerminatingAggregationOperation<T> {
 
+		/**
+		 * Map the query result to a different type using {@link QueryResultConverter}.
+		 *
+		 * @param <R> {@link Class type} of the result.
+		 * @param converter the converter, must not be {@literal null}.
+		 * @return new instance of {@link ExecutableFindOperation.TerminatingFindNear}.
+		 * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}.
+		 * @since 5.0
+		 */
+		@Contract("_ -> new")
+		<R> TerminatingAggregationOperation<R> map(QueryResultConverter<? super T, ? extends R> converter);
+
 		/**
 		 * Apply pipeline operations as specified and stream all matching elements. <br />
 		 *
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupport.java
index 954fd61716..fbaff2bc39 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupport.java
@@ -15,6 +15,7 @@
  */
 package org.springframework.data.mongodb.core;
 
+import org.jspecify.annotations.Nullable;
 import reactor.core.publisher.Flux;
 
 import org.springframework.data.mongodb.core.aggregation.Aggregation;
@@ -51,22 +52,25 @@ public <T> ReactiveAggregation<T> aggregateAndReturn(Class<T> domainType) {
 
 		Assert.notNull(domainType, "DomainType must not be null");
 
-		return new ReactiveAggregationSupport<>(template, domainType, null, null);
+		return new ReactiveAggregationSupport<>(template, domainType, QueryResultConverter.entity(), null, null);
 	}
 
-	static class ReactiveAggregationSupport<T>
+	static class ReactiveAggregationSupport<S, T>
 			implements AggregationOperationWithAggregation<T>, ReactiveAggregation<T>, TerminatingAggregationOperation<T> {
 
 		private final ReactiveMongoTemplate template;
-		private final Class<T> domainType;
-		private final Aggregation aggregation;
-		private final String collection;
+		private final Class<S> domainType;
+		private final QueryResultConverter<? super S, ? extends T> resultConverter;
+		private final @Nullable Aggregation aggregation;
+		private final @Nullable String collection;
 
-		ReactiveAggregationSupport(ReactiveMongoTemplate template, Class<T> domainType, Aggregation aggregation,
-				String collection) {
+		ReactiveAggregationSupport(ReactiveMongoTemplate template, Class<S> domainType,
+				QueryResultConverter<? super S, ? extends T> resultConverter, @Nullable Aggregation aggregation,
+				@Nullable String collection) {
 
 			this.template = template;
 			this.domainType = domainType;
+			this.resultConverter = resultConverter;
 			this.aggregation = aggregation;
 			this.collection = collection;
 		}
@@ -76,7 +80,7 @@ public AggregationOperationWithAggregation<T> inCollection(String collection) {
 
 			Assert.hasText(collection, "Collection must not be null nor empty");
 
-			return new ReactiveAggregationSupport<>(template, domainType, aggregation, collection);
+			return new ReactiveAggregationSupport<>(template, domainType, resultConverter, aggregation, collection);
 		}
 
 		@Override
@@ -84,12 +88,24 @@ public TerminatingAggregationOperation<T> by(Aggregation aggregation) {
 
 			Assert.notNull(aggregation, "Aggregation must not be null");
 
-			return new ReactiveAggregationSupport<>(template, domainType, aggregation, collection);
+			return new ReactiveAggregationSupport<>(template, domainType, resultConverter, aggregation, collection);
+		}
+
+		@Override
+		public <R> TerminatingAggregationOperation<R> map(QueryResultConverter<? super T, ? extends R> converter) {
+
+			Assert.notNull(converter, "QueryResultConverter must not be null");
+
+			return new ReactiveAggregationSupport<>(template, domainType, resultConverter.andThen(converter), aggregation,
+					collection);
 		}
 
 		@Override
 		public Flux<T> all() {
-			return template.aggregate(aggregation, getCollectionName(aggregation), domainType);
+
+			Assert.notNull(aggregation, "Aggregation must be set first");
+
+			return template.doAggregate(aggregation, getCollectionName(aggregation), domainType, domainType, resultConverter);
 		}
 
 		private String getCollectionName(Aggregation aggregation) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveChangeStreamOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveChangeStreamOperationSupport.java
index afeb6c5e0e..589f264f17 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveChangeStreamOperationSupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveChangeStreamOperationSupport.java
@@ -24,11 +24,11 @@
 import org.bson.BsonTimestamp;
 import org.bson.BsonValue;
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.ChangeStreamOptions.ChangeStreamOptionsBuilder;
 import org.springframework.data.mongodb.core.aggregation.Aggregation;
 import org.springframework.data.mongodb.core.aggregation.MatchOperation;
 import org.springframework.data.mongodb.core.query.CriteriaDefinition;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
 /**
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperation.java
index cba827ffed..eaa9da4a37 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperation.java
@@ -25,6 +25,7 @@
 import org.springframework.data.mongodb.core.query.CriteriaDefinition;
 import org.springframework.data.mongodb.core.query.NearQuery;
 import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.lang.Contract;
 
 /**
  * {@link ReactiveFindOperation} allows creation and execution of reactive MongoDB find operations in a fluent API
@@ -66,7 +67,28 @@ public interface ReactiveFindOperation {
 	/**
 	 * Compose find execution by calling one of the terminating methods.
 	 */
-	interface TerminatingFind<T> {
+	interface TerminatingFind<T> extends TerminatingResults<T>, TerminatingProjection {
+
+	}
+
+	/**
+	 * Compose find execution by calling one of the terminating methods.
+	 *
+	 * @since 5.0
+	 */
+	interface TerminatingResults<T> {
+
+		/**
+		 * Map the query result to a different type using {@link QueryResultConverter}.
+		 *
+		 * @param <R> {@link Class type} of the result.
+		 * @param converter the converter, must not be {@literal null}.
+		 * @return new instance of {@link TerminatingResults}.
+		 * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}.
+		 * @since 5.0
+		 */
+		@Contract("_ -> new")
+		<R> TerminatingResults<R> map(QueryResultConverter<? super T, ? extends R> converter);
 
 		/**
 		 * Get exactly zero or one result.
@@ -95,7 +117,7 @@ interface TerminatingFind<T> {
 		 * <p>
 		 * When using {@link KeysetScrollPosition}, make sure to use non-nullable
 		 * {@link org.springframework.data.domain.Sort sort properties} as MongoDB does not support criteria to reconstruct
-		 * a query result from absent document fields or {@code null} values through {@code $gt/$lt} operators.
+		 * a query result from absent document fields or {@literal null} values through {@code $gt/$lt} operators.
 		 *
 		 * @param scrollPosition the scroll position.
 		 * @return a scroll of the resulting elements.
@@ -120,6 +142,15 @@ interface TerminatingFind<T> {
 		 */
 		Flux<T> tail();
 
+	}
+
+	/**
+	 * Compose find execution by calling one of the terminating methods.
+	 *
+	 * @since 5.0
+	 */
+	interface TerminatingProjection {
+
 		/**
 		 * Get the number of matching elements. <br />
 		 * This method uses an
@@ -145,6 +176,18 @@ interface TerminatingFind<T> {
 	 */
 	interface TerminatingFindNear<T> {
 
+		/**
+		 * Map the query result to a different type using {@link QueryResultConverter}.
+		 *
+		 * @param <R> {@link Class type} of the result.
+		 * @param converter the converter, must not be {@literal null}.
+		 * @return new instance of {@link ExecutableFindOperation.TerminatingFindNear}.
+		 * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}.
+		 * @since 5.0
+		 */
+		@Contract("_ -> new")
+		<R> TerminatingFindNear<R> map(QueryResultConverter<? super T, ? extends R> converter);
+
 		/**
 		 * Find all matching elements and return them as {@link org.springframework.data.geo.GeoResult}.
 		 *
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupport.java
index d1aec8af36..38e32dc977 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupport.java
@@ -19,14 +19,15 @@
 import reactor.core.publisher.Mono;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.dao.IncorrectResultSizeDataAccessException;
-import org.springframework.data.domain.Window;
 import org.springframework.data.domain.ScrollPosition;
+import org.springframework.data.domain.Window;
+import org.springframework.data.geo.GeoResult;
 import org.springframework.data.mongodb.core.CollectionPreparerSupport.ReactiveCollectionPreparerDelegate;
 import org.springframework.data.mongodb.core.query.NearQuery;
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mongodb.core.query.SerializationUtils;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 
@@ -52,7 +53,7 @@ public <T> ReactiveFind<T> query(Class<T> domainType) {
 
 		Assert.notNull(domainType, "DomainType must not be null");
 
-		return new ReactiveFindSupport<>(template, domainType, domainType, null, ALL_QUERY);
+		return new ReactiveFindSupport<>(template, domainType, domainType, QueryResultConverter.entity(), null, ALL_QUERY);
 	}
 
 	/**
@@ -61,21 +62,24 @@ public <T> ReactiveFind<T> query(Class<T> domainType) {
 	 * @author Christoph Strobl
 	 * @since 2.0
 	 */
-	static class ReactiveFindSupport<T>
+	static class ReactiveFindSupport<S, T>
 			implements ReactiveFind<T>, FindWithCollection<T>, FindWithProjection<T>, FindWithQuery<T> {
 
 		private final ReactiveMongoTemplate template;
 		private final Class<?> domainType;
-		private final Class<T> returnType;
-		private final String collection;
+		private final Class<S> returnType;
+		private final QueryResultConverter<? super S, ? extends T> resultConverter;
+		private final @Nullable String collection;
 		private final Query query;
 
-		ReactiveFindSupport(ReactiveMongoTemplate template, Class<?> domainType, Class<T> returnType, String collection,
+		ReactiveFindSupport(ReactiveMongoTemplate template, Class<?> domainType, Class<S> returnType,
+				QueryResultConverter<? super S, ? extends T> resultConverter, @Nullable String collection,
 				Query query) {
 
 			this.template = template;
 			this.domainType = domainType;
 			this.returnType = returnType;
+			this.resultConverter = resultConverter;
 			this.collection = collection;
 			this.query = query;
 		}
@@ -85,7 +89,7 @@ public FindWithProjection<T> inCollection(String collection) {
 
 			Assert.hasText(collection, "Collection name must not be null nor empty");
 
-			return new ReactiveFindSupport<>(template, domainType, returnType, collection, query);
+			return new ReactiveFindSupport<>(template, domainType, returnType, resultConverter, collection, query);
 		}
 
 		@Override
@@ -93,7 +97,8 @@ public <T1> FindWithQuery<T1> as(Class<T1> returnType) {
 
 			Assert.notNull(returnType, "ReturnType must not be null");
 
-			return new ReactiveFindSupport<>(template, domainType, returnType, collection, query);
+			return new ReactiveFindSupport<>(template, domainType, returnType, QueryResultConverter.entity(), collection,
+					query);
 		}
 
 		@Override
@@ -101,7 +106,16 @@ public TerminatingFind<T> matching(Query query) {
 
 			Assert.notNull(query, "Query must not be null");
 
-			return new ReactiveFindSupport<>(template, domainType, returnType, collection, query);
+			return new ReactiveFindSupport<>(template, domainType, returnType, resultConverter, collection, query);
+		}
+
+		@Override
+		public <R> TerminatingResults<R> map(QueryResultConverter<? super T, ? extends R> converter) {
+
+			Assert.notNull(converter, "QueryResultConverter must not be null");
+
+			return new ReactiveFindSupport<>(template, domainType, returnType, this.resultConverter.andThen(converter),
+					collection, query);
 		}
 
 		@Override
@@ -141,7 +155,8 @@ public Flux<T> all() {
 
 		@Override
 		public Mono<Window<T>> scroll(ScrollPosition scrollPosition) {
-			return template.doScroll(query.with(scrollPosition), domainType, returnType, getCollectionName());
+			return template.doScroll(query.with(scrollPosition), domainType, returnType, resultConverter,
+					getCollectionName());
 		}
 
 		@Override
@@ -151,7 +166,7 @@ public Flux<T> tail() {
 
 		@Override
 		public TerminatingFindNear<T> near(NearQuery nearQuery) {
-			return () -> template.geoNear(nearQuery, domainType, getCollectionName(), returnType);
+			return new TerminatingFindNearSupport<>(nearQuery, resultConverter);
 		}
 
 		@Override
@@ -178,14 +193,15 @@ private Flux<T> doFind(@Nullable FindPublisherPreparer preparer) {
 			Document fieldsObject = query.getFieldsObject();
 
 			return template.doFind(getCollectionName(), ReactiveCollectionPreparerDelegate.of(query), queryObject,
-					fieldsObject, domainType, returnType, preparer != null ? preparer : getCursorPreparer(query));
+					fieldsObject, domainType, returnType, resultConverter,
+					preparer != null ? preparer : getCursorPreparer(query));
 		}
 
-		@SuppressWarnings("unchecked")
+		@SuppressWarnings({ "unchecked", "rawtypes" })
 		private Flux<T> doFindDistinct(String field) {
 
 			return template.findDistinct(query, field, getCollectionName(), domainType,
-					returnType == domainType ? (Class<T>) Object.class : returnType);
+					returnType == domainType ? (Class) Object.class : returnType);
 		}
 
 		private FindPublisherPreparer getCursorPreparer(Query query) {
@@ -200,10 +216,36 @@ private String asString() {
 			return SerializationUtils.serializeToJsonSafely(query);
 		}
 
+		class TerminatingFindNearSupport<G> implements TerminatingFindNear<G> {
+
+			private final NearQuery nearQuery;
+			private final QueryResultConverter<? super S, ? extends G> resultConverter;
+
+			public TerminatingFindNearSupport(NearQuery nearQuery,
+					QueryResultConverter<? super S, ? extends G> resultConverter) {
+				this.nearQuery = nearQuery;
+				this.resultConverter = resultConverter;
+			}
+
+			@Override
+			public <R> TerminatingFindNear<R> map(QueryResultConverter<? super G, ? extends R> converter) {
+
+				Assert.notNull(converter, "QueryResultConverter must not be null");
+
+				return new TerminatingFindNearSupport<>(nearQuery, this.resultConverter.andThen(converter));
+			}
+
+			@Override
+			public Flux<GeoResult<G>> all() {
+				return template.doGeoNear(nearQuery, domainType, getCollectionName(), returnType, resultConverter);
+			}
+		}
+
 		/**
 		 * @author Christoph Strobl
 		 * @since 2.1
 		 */
+		@SuppressWarnings({ "unchecked", "rawtypes" })
 		static class DistinctOperationSupport<T> implements TerminatingDistinct<T> {
 
 			private final String field;
@@ -224,12 +266,11 @@ public <R> TerminatingDistinct<R> as(Class<R> resultType) {
 			}
 
 			@Override
-			@SuppressWarnings("unchecked")
 			public TerminatingDistinct<T> matching(Query query) {
 
 				Assert.notNull(query, "Query must not be null");
 
-				return new DistinctOperationSupport<>((ReactiveFindSupport<T>) delegate.matching(query), field);
+				return new DistinctOperationSupport<>((ReactiveFindSupport) delegate.matching(query), field);
 			}
 
 			@Override
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveInsertOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveInsertOperationSupport.java
index 06d3c6eae7..9d424c2446 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveInsertOperationSupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveInsertOperationSupport.java
@@ -15,6 +15,7 @@
  */
 package org.springframework.data.mongodb.core;
 
+import org.jspecify.annotations.Nullable;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
@@ -50,9 +51,9 @@ static class ReactiveInsertSupport<T> implements ReactiveInsert<T> {
 
 		private final ReactiveMongoTemplate template;
 		private final Class<T> domainType;
-		private final String collection;
+		private final @Nullable String collection;
 
-		ReactiveInsertSupport(ReactiveMongoTemplate template, Class<T> domainType, String collection) {
+		ReactiveInsertSupport(ReactiveMongoTemplate template, Class<T> domainType, @Nullable String collection) {
 
 			this.template = template;
 			this.domainType = domainType;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupport.java
index 4f0d395950..4e3379bad0 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupport.java
@@ -17,9 +17,9 @@
 
 import reactor.core.publisher.Flux;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
 import org.springframework.data.mongodb.core.query.Query;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 
@@ -89,8 +89,11 @@ static class ReactiveMapReduceSupport<T>
 		@Override
 		public Flux<T> all() {
 
+			Assert.notNull(mapFunction, "MapFunction must be set first");
+			Assert.notNull(reduceFunction, "ReduceFunction must be set first");
+
 			return template.mapReduce(query, domainType, getCollectionName(), returnType, mapFunction, reduceFunction,
-					options);
+					options != null ? options : MapReduceOptions.options());
 		}
 
 		/*
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoClientFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoClientFactoryBean.java
index 89d1cd78ac..89caf3273c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoClientFactoryBean.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoClientFactoryBean.java
@@ -16,10 +16,10 @@
 
 package org.springframework.data.mongodb.core;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.beans.factory.config.AbstractFactoryBean;
 import org.springframework.dao.DataAccessException;
 import org.springframework.dao.support.PersistenceExceptionTranslator;
-import org.springframework.lang.Nullable;
 import org.springframework.util.StringUtils;
 
 import com.mongodb.MongoClientSettings;
@@ -89,7 +89,7 @@ public void setExceptionTranslator(@Nullable PersistenceExceptionTranslator exce
 	}
 
 	@Override
-	public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
+	public @Nullable DataAccessException translateExceptionIfPossible(RuntimeException ex) {
 		return exceptionTranslator.translateExceptionIfPossible(ex);
 	}
 
@@ -124,7 +124,9 @@ protected MongoClient createInstance() throws Exception {
 
 	@Override
 	protected void destroyInstance(@Nullable MongoClient instance) throws Exception {
-		instance.close();
+		if (instance != null) {
+			instance.close();
+		}
 	}
 
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java
index 90f2d2345d..14f6ee2631 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java
@@ -23,6 +23,7 @@
 import java.util.function.Supplier;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.reactivestreams.Publisher;
 import org.reactivestreams.Subscription;
 import org.springframework.data.domain.KeysetScrollPosition;
@@ -48,7 +49,6 @@
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mongodb.core.query.Update;
 import org.springframework.data.mongodb.core.query.UpdateDefinition;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 
@@ -513,7 +513,7 @@ Mono<MongoCollection<Document>> createView(String name, String source, Aggregati
 	 * <p>
 	 * When using {@link KeysetScrollPosition}, make sure to use non-nullable {@link org.springframework.data.domain.Sort
 	 * sort properties} as MongoDB does not support criteria to reconstruct a query result from absent document fields or
-	 * {@code null} values through {@code $gt/$lt} operators.
+	 * {@literal null} values through {@code $gt/$lt} operators.
 	 *
 	 * @param query the query class that specifies the criteria used to find a document and also an optional fields
 	 *          specification. Must not be {@literal null}.
@@ -538,7 +538,7 @@ Mono<MongoCollection<Document>> createView(String name, String source, Aggregati
 	 * <p>
 	 * When using {@link KeysetScrollPosition}, make sure to use non-nullable {@link org.springframework.data.domain.Sort
 	 * sort properties} as MongoDB does not support criteria to reconstruct a query result from absent document fields or
-	 * {@code null} values through {@code $gt/$lt} operators.
+	 * {@literal null} values through {@code $gt/$lt} operators.
 	 *
 	 * @param query the query class that specifies the criteria used to find a document and also an optional fields
 	 *          specification. Must not be {@literal null}.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java
index ea427a3e1f..0ad473b8b7 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java
@@ -44,9 +44,9 @@
 import org.bson.Document;
 import org.bson.conversions.Bson;
 import org.bson.types.ObjectId;
+import org.jspecify.annotations.Nullable;
 import org.reactivestreams.Publisher;
 import org.reactivestreams.Subscriber;
-
 import org.springframework.beans.BeansException;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.ApplicationContextAware;
@@ -113,15 +113,15 @@
 import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
 import org.springframework.data.mongodb.core.query.BasicQuery;
 import org.springframework.data.mongodb.core.query.Collation;
+import org.springframework.data.mongodb.core.query.Criteria;
 import org.springframework.data.mongodb.core.query.Meta;
 import org.springframework.data.mongodb.core.query.NearQuery;
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mongodb.core.query.UpdateDefinition;
 import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter;
-import org.springframework.data.mongodb.util.MongoCompatibilityAdapter;
 import org.springframework.data.projection.EntityProjection;
 import org.springframework.data.util.Optionals;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.CollectionUtils;
@@ -345,7 +345,8 @@ public void setWriteConcern(@Nullable WriteConcern writeConcern) {
 	 * @param writeConcernResolver can be {@literal null}.
 	 */
 	public void setWriteConcernResolver(@Nullable WriteConcernResolver writeConcernResolver) {
-		this.writeConcernResolver = writeConcernResolver;
+		this.writeConcernResolver = writeConcernResolver != null ? writeConcernResolver
+				: DefaultWriteConcernResolver.INSTANCE;
 	}
 
 	/**
@@ -739,10 +740,11 @@ public <T> Mono<Boolean> collectionExists(Class<T> entityClass) {
 
 	@Override
 	public Mono<Boolean> collectionExists(String collectionName) {
-		return createMono(db -> Flux.from(MongoCompatibilityAdapter.reactiveMongoDatabaseAdapter().forDb(db).listCollectionNames()) //
-				.filter(s -> s.equals(collectionName)) //
-				.map(s -> true) //
-				.single(false));
+		return createMono(
+				db -> Flux.from(db.listCollectionNames()) //
+						.filter(s -> s.equals(collectionName)) //
+						.map(s -> true) //
+						.single(false));
 	}
 
 	@Override
@@ -787,7 +789,7 @@ public ReactiveBulkOperations bulkOps(BulkMode mode, @Nullable Class<?> entityTy
 
 	@Override
 	public Flux<String> getCollectionNames() {
-		return createFlux(db -> MongoCompatibilityAdapter.reactiveMongoDatabaseAdapter().forDb(db).listCollectionNames());
+		return createFlux(db -> db.listCollectionNames());
 	}
 
 	public Mono<MongoDatabase> getMongoDatabase() {
@@ -877,10 +879,11 @@ public <T> Mono<Window<T>> scroll(Query query, Class<T> entityType) {
 
 	@Override
 	public <T> Mono<Window<T>> scroll(Query query, Class<T> entityType, String collectionName) {
-		return doScroll(query, entityType, entityType, collectionName);
+		return doScroll(query, entityType, entityType, QueryResultConverter.entity(), collectionName);
 	}
 
-	<T> Mono<Window<T>> doScroll(Query query, Class<?> sourceClass, Class<T> targetClass, String collectionName) {
+	<T, R> Mono<Window<R>> doScroll(Query query, Class<?> sourceClass, Class<T> targetClass,
+			QueryResultConverter<? super T, ? extends R> resultConverter, String collectionName) {
 
 		Assert.notNull(query, "Query must not be null");
 		Assert.notNull(collectionName, "CollectionName must not be null");
@@ -888,7 +891,7 @@ <T> Mono<Window<T>> doScroll(Query query, Class<?> sourceClass, Class<T> targetC
 		Assert.notNull(targetClass, "Target type must not be null");
 
 		EntityProjection<T, ?> projection = operations.introspectProjection(targetClass, sourceClass);
-		ProjectingReadCallback<?, T> callback = new ProjectingReadCallback<>(mongoConverter, projection, collectionName);
+		DocumentCallback<R> callback = getResultReader(projection, collectionName, resultConverter);
 		int limit = query.isLimited() ? query.getLimit() + 1 : Integer.MAX_VALUE;
 
 		if (query.hasKeyset()) {
@@ -896,18 +899,18 @@ <T> Mono<Window<T>> doScroll(Query query, Class<?> sourceClass, Class<T> targetC
 			KeysetScrollQuery keysetPaginationQuery = ScrollUtils.createKeysetPaginationQuery(query,
 					operations.getIdPropertyName(sourceClass));
 
-			Mono<List<T>> result = doFind(collectionName, ReactiveCollectionPreparerDelegate.of(query),
+			Mono<List<R>> result = doFind(collectionName, ReactiveCollectionPreparerDelegate.of(query),
 					keysetPaginationQuery.query(), keysetPaginationQuery.fields(), sourceClass,
 					new QueryFindPublisherPreparer(query, keysetPaginationQuery.sort(), limit, 0, sourceClass), callback)
-							.collectList();
+					.collectList();
 
 			return result.map(it -> ScrollUtils.createWindow(query, it, sourceClass, operations));
 		}
 
-		Mono<List<T>> result = doFind(collectionName, ReactiveCollectionPreparerDelegate.of(query), query.getQueryObject(),
+		Mono<List<R>> result = doFind(collectionName, ReactiveCollectionPreparerDelegate.of(query), query.getQueryObject(),
 				query.getFieldsObject(), sourceClass,
 				new QueryFindPublisherPreparer(query, query.getSortObject(), limit, query.getSkip(), sourceClass), callback)
-						.collectList();
+				.collectList();
 
 		return result.map(
 				it -> ScrollUtils.createWindow(it, query.getLimit(), OffsetScrollPosition.positionFunction(query.getSkip())));
@@ -1003,6 +1006,11 @@ public <O> Flux<O> aggregate(Aggregation aggregation, String collectionName, Cla
 
 	protected <O> Flux<O> doAggregate(Aggregation aggregation, String collectionName, @Nullable Class<?> inputType,
 			Class<O> outputType) {
+		return doAggregate(aggregation, collectionName, inputType, outputType, QueryResultConverter.entity());
+	}
+
+	<T, O> Flux<O> doAggregate(Aggregation aggregation, String collectionName, @Nullable Class<?> inputType,
+			Class<T> outputType, QueryResultConverter<? super T, ? extends O> resultConverter) {
 
 		Assert.notNull(aggregation, "Aggregation pipeline must not be null");
 		Assert.hasText(collectionName, "Collection name must not be null or empty");
@@ -1018,13 +1026,14 @@ protected <O> Flux<O> doAggregate(Aggregation aggregation, String collectionName
 					serializeToJsonSafely(ctx.getAggregationPipeline()), collectionName));
 		}
 
-		ReadDocumentCallback<O> readCallback = new ReadDocumentCallback<>(mongoConverter, outputType, collectionName);
+		DocumentCallback<O> readCallback = new QueryResultConverterCallback<>(resultConverter,
+				new ReadDocumentCallback<>(mongoConverter, outputType, collectionName));
 		return execute(collectionName, collection -> aggregateAndMap(collection, ctx.getAggregationPipeline(),
 				ctx.isOutOrMerge(), options, readCallback, ctx.getInputType()));
 	}
 
 	private <O> Flux<O> aggregateAndMap(MongoCollection<Document> collection, List<Document> pipeline,
-			boolean isOutOrMerge, AggregationOptions options, ReadDocumentCallback<O> readCallback,
+			boolean isOutOrMerge, AggregationOptions options, DocumentCallback<O> readCallback,
 			@Nullable Class<?> inputType) {
 
 		ReactiveCollectionPreparerDelegate collectionPreparer = ReactiveCollectionPreparerDelegate.of(options);
@@ -1070,9 +1079,14 @@ public <T> Flux<GeoResult<T>> geoNear(NearQuery near, Class<T> entityClass, Stri
 		return geoNear(near, entityClass, collectionName, entityClass);
 	}
 
-	@SuppressWarnings("unchecked")
 	protected <T> Flux<GeoResult<T>> geoNear(NearQuery near, Class<?> entityClass, String collectionName,
 			Class<T> returnType) {
+		return doGeoNear(near, entityClass, collectionName, returnType, QueryResultConverter.entity());
+	}
+
+	@SuppressWarnings("unchecked")
+	<T, R> Flux<GeoResult<R>> doGeoNear(NearQuery near, Class<?> entityClass, String collectionName, Class<T> returnType,
+			QueryResultConverter<? super T, ? extends R> resultConverter) {
 
 		if (near == null) {
 			throw new InvalidDataAccessApiUsageException("NearQuery must not be null");
@@ -1086,8 +1100,8 @@ protected <T> Flux<GeoResult<T>> geoNear(NearQuery near, Class<?> entityClass, S
 		String distanceField = operations.nearQueryDistanceFieldName(entityClass);
 		EntityProjection<T, ?> projection = operations.introspectProjection(returnType, entityClass);
 
-		GeoNearResultDocumentCallback<T> callback = new GeoNearResultDocumentCallback<>(distanceField,
-				new ProjectingReadCallback<>(mongoConverter, projection, collection), near.getMetric());
+		GeoNearResultDocumentCallback<R> callback = new GeoNearResultDocumentCallback<>(distanceField,
+				getResultReader(projection, collectionName, resultConverter), near.getMetric());
 
 		Builder optionsBuilder = AggregationOptions.builder();
 		if (near.hasReadPreference()) {
@@ -1123,9 +1137,8 @@ public <T> Mono<T> findAndModify(Query query, UpdateDefinition update, FindAndMo
 		return findAndModify(query, update, options, entityClass, getCollectionName(entityClass));
 	}
 
-	@Override
-	public <T> Mono<T> findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options,
-			Class<T> entityClass, String collectionName) {
+	public <S, T> Mono<T> findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options,
+			Class<S> entityClass, String collectionName, QueryResultConverter<? super S, ? extends T> resultConverter) {
 
 		Assert.notNull(options, "Options must not be null ");
 		Assert.notNull(entityClass, "Entity class must not be null");
@@ -1142,12 +1155,27 @@ public <T> Mono<T> findAndModify(Query query, UpdateDefinition update, FindAndMo
 		}
 
 		return doFindAndModify(collectionName, ReactiveCollectionPreparerDelegate.of(query), query.getQueryObject(),
-				query.getFieldsObject(), getMappedSortObject(query, entityClass), entityClass, update, optionsToUse);
+				query.getFieldsObject(), getMappedSortObject(query, entityClass), entityClass, update, optionsToUse,
+				resultConverter);
+	}
+
+	@Override
+	public <T> Mono<T> findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options,
+			Class<T> entityClass, String collectionName) {
+		return findAndModify(query, update, options, entityClass, collectionName, QueryResultConverter.entity());
 	}
 
 	@Override
 	public <S, T> Mono<T> findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class<S> entityType,
 			String collectionName, Class<T> resultType) {
+		return findAndReplace(query, replacement, options, entityType, collectionName, resultType,
+				QueryResultConverter.entity());
+	}
+
+	@SuppressWarnings("NullAway")
+	public <S, T, R> Mono<R> findAndReplace(Query query, S replacement, FindAndReplaceOptions options,
+			Class<S> entityType, String collectionName, Class<T> resultType,
+			QueryResultConverter<? super T, ? extends R> resultConverter) {
 
 		Assert.notNull(query, "Query must not be null");
 		Assert.notNull(replacement, "Replacement must not be null");
@@ -1184,9 +1212,9 @@ public <S, T> Mono<T> findAndReplace(Query query, S replacement, FindAndReplaceO
 								mapped.getCollection()));
 			}).flatMap(it -> {
 
-				Mono<T> afterFindAndReplace = doFindAndReplace(it.getCollection(), collectionPreparer, mappedQuery,
+				Mono<R> afterFindAndReplace = doFindAndReplace(it.getCollection(), collectionPreparer, mappedQuery,
 						mappedFields, mappedSort, queryContext.getCollation(entityType).orElse(null), entityType, it.getTarget(),
-						options, projection);
+						options, projection, resultConverter);
 				return afterFindAndReplace.flatMap(saved -> {
 					maybeEmitEvent(new AfterSaveEvent<>(saved, it.getTarget(), it.getCollection()));
 					return maybeCallAfterSave(saved, it.getTarget(), it.getCollection());
@@ -1351,6 +1379,7 @@ public <T> Mono<T> insert(T objectToSave, String collectionName) {
 		return doInsert(collectionName, objectToSave, this.mongoConverter);
 	}
 
+	@SuppressWarnings("NullAway")
 	protected <T> Mono<T> doInsert(String collectionName, T objectToSave, MongoWriter<Object> writer) {
 
 		return Mono.just(PersistableEntityModel.of(objectToSave, collectionName)) //
@@ -1401,6 +1430,7 @@ public <T> Flux<T> insertAll(Mono<? extends Collection<? extends T>> objectsToSa
 		return Flux.from(objectsToSave).flatMapSequential(this::insertAll);
 	}
 
+	@SuppressWarnings("NullAway")
 	protected <T> Flux<T> doInsertAll(Collection<? extends T> listToSave, MongoWriter<Object> writer) {
 
 		Map<String, List<T>> elementsByCollection = new HashMap<>();
@@ -1417,6 +1447,7 @@ protected <T> Flux<T> doInsertAll(Collection<? extends T> listToSave, MongoWrite
 				.concatMap(collectionName -> doInsertBatch(collectionName, elementsByCollection.get(collectionName), writer));
 	}
 
+	@SuppressWarnings("NullAway")
 	protected <T> Flux<T> doInsertBatch(String collectionName, Collection<? extends T> batchToSave,
 			MongoWriter<Object> writer) {
 
@@ -1434,11 +1465,16 @@ protected <T> Flux<T> doInsertBatch(String collectionName, Collection<? extends
 						entity.assertUpdateableIdIfNotSet();
 
 						T initialized = entity.initializeVersionProperty();
-						Document dbDoc = entity.toMappedDocument(writer).getDocument();
+						MappedDocument mapped = entity.toMappedDocument(writer);
 
-						maybeEmitEvent(new BeforeSaveEvent<>(initialized, dbDoc, collectionName));
+						maybeEmitEvent(new BeforeSaveEvent<>(initialized, mapped.getDocument(), collectionName));
+						return maybeCallBeforeSave(initialized, mapped.getDocument(), collectionName).map(toSave -> {
 
-						return maybeCallBeforeSave(initialized, dbDoc, collectionName).thenReturn(Tuples.of(entity, dbDoc));
+							MappedDocument mappedDocument = queryOperations.createInsertContext(mapped)
+									.prepareId(uninitialized.getClass());
+
+							return Tuples.of(entity, mappedDocument.getDocument());
+						});
 					});
 				}).collectList();
 
@@ -1532,6 +1568,7 @@ private <T> Mono<T> doSaveVersioned(AdaptibleEntity<T> source, String collection
 		});
 	}
 
+	@SuppressWarnings("NullAway")
 	protected <T> Mono<T> doSave(String collectionName, T objectToSave, MongoWriter<Object> writer) {
 
 		assertUpdateableIdIfNotSet(objectToSave);
@@ -1625,6 +1662,7 @@ private MongoCollection<Document> prepareCollection(MongoCollection<Document> co
 		return collectionToUse;
 	}
 
+	@SuppressWarnings("NullAway")
 	protected Mono<Object> saveDocument(String collectionName, Document document, Class<?> entityClass) {
 
 		if (LOGGER.isDebugEnabled()) {
@@ -1728,7 +1766,8 @@ public Mono<UpdateResult> updateMulti(Query query, UpdateDefinition update, Clas
 		return doUpdate(collectionName, query, update, entityClass, false, true);
 	}
 
-	protected Mono<UpdateResult> doUpdate(String collectionName, Query query, @Nullable UpdateDefinition update,
+	@SuppressWarnings("NullAway")
+	protected Mono<UpdateResult> doUpdate(String collectionName, Query query, UpdateDefinition update,
 			@Nullable Class<?> entityClass, boolean upsert, boolean multi) {
 
 		MongoPersistentEntity<?> entity = entityClass == null ? null : getPersistentEntity(entityClass);
@@ -1810,7 +1849,8 @@ protected Mono<UpdateResult> doUpdate(String collectionName, Query query, @Nulla
 
 					Document updateObj = updateContext.getMappedUpdate(entity);
 					if (containsVersionProperty(queryObj, entity))
-						throw new OptimisticLockingFailureException("Optimistic lock exception on saving entity %s to collection %s".formatted(entity.getName(),  collectionName));
+						throw new OptimisticLockingFailureException("Optimistic lock exception on saving entity %s to collection %s"
+								.formatted(entity.getName(), collectionName));
 				}
 			}
 		});
@@ -2008,18 +2048,18 @@ public <T> Flux<T> tail(Query query, Class<T> entityClass) {
 	@Override
 	public <T> Flux<T> tail(@Nullable Query query, Class<T> entityClass, String collectionName) {
 
-		ReactiveCollectionPreparerDelegate collectionPreparer = ReactiveCollectionPreparerDelegate.of(query);
 		if (query == null) {
 
 			LOGGER.debug(String.format("Tail for class: %s in collection: %s", entityClass, collectionName));
 
 			return executeFindMultiInternal(
-					collection -> new FindCallback(collectionPreparer, null).doInCollection(collection)
+					collection -> new FindCallback(CollectionPreparer.identity(), null).doInCollection(collection)
 							.cursorType(CursorType.TailableAwait),
 					FindPublisherPreparer.NO_OP_PREPARER, new ReadDocumentCallback<>(mongoConverter, entityClass, collectionName),
 					collectionName);
 		}
 
+		ReactiveCollectionPreparerDelegate collectionPreparer = ReactiveCollectionPreparerDelegate.of(query);
 		return doFind(collectionName, collectionPreparer, query.getQueryObject(), query.getFieldsObject(), entityClass,
 				new TailingQueryFindPublisherPreparer(query, entityClass));
 	}
@@ -2171,10 +2211,6 @@ public <T> Flux<T> mapReduce(Query filterQuery, Class<?> domainType, String inpu
 				publisher = publisher.jsMode(options.getJavaScriptMode());
 			}
 
-			if (options.getOutputSharded().isPresent()) {
-				MongoCompatibilityAdapter.mapReducePublisherAdapter(publisher).sharded(options.getOutputSharded().get());
-			}
-
 			if (StringUtils.hasText(options.getOutputCollection()) && !options.usesInlineOutput()) {
 				publisher = publisher.collectionName(options.getOutputCollection()).action(options.getMapReduceAction());
 
@@ -2257,6 +2293,44 @@ protected <T> Flux<T> doFindAndDelete(String collectionName, Query query, Class<
 						.flatMapSequential(deleteResult -> Flux.fromIterable(list)));
 	}
 
+	@SuppressWarnings({"rawtypes", "unchecked", "NullAway"})
+	<S, T> Flux<T> doFindAndDelete(String collectionName, Query query, Class<S> entityClass,
+			QueryResultConverter<? super S, ? extends T> resultConverter) {
+
+		List<Object> ids = new ArrayList<>();
+		ProjectingReadCallback readCallback = new ProjectingReadCallback(getConverter(),
+				EntityProjection.nonProjecting(entityClass), collectionName);
+
+		QueryResultConverterCallback<S, T> callback = new QueryResultConverterCallback<>(resultConverter, readCallback) {
+
+			@Override
+			public Mono<T> doWith(Document object) {
+				ids.add(object.get("_id"));
+				return super.doWith(object);
+			}
+		};
+
+		Flux<T> flux = doFind(collectionName, ReactiveCollectionPreparerDelegate.of(query), query.getQueryObject(),
+				query.getFieldsObject(), entityClass,
+				new QueryFindPublisherPreparer(query, query.getSortObject(), query.getLimit(), query.getSkip(), entityClass),
+				callback);
+
+		return Flux.from(flux).collectList().filter(it -> !it.isEmpty()).flatMapMany(list -> {
+
+			Criteria[] criterias = ids.stream() //
+					.map(it -> Criteria.where("_id").is(it)) //
+					.toArray(Criteria[]::new);
+
+			Query removeQuery = new Query(criterias.length == 1 ? criterias[0] : new Criteria().orOperator(criterias));
+			if (query.hasReadPreference()) {
+				removeQuery.withReadPreference(query.getReadPreference());
+			}
+
+			return Flux.from(remove(removeQuery, entityClass, collectionName))
+					.flatMapSequential(deleteResult -> Flux.fromIterable(list));
+		});
+	}
+
 	/**
 	 * Create the specified collection using the provided options
 	 *
@@ -2382,8 +2456,8 @@ protected <S, T> Flux<T> doFind(String collectionName,
 					serializeToJsonSafely(mappedQuery), mappedFields, entityClass, collectionName));
 		}
 
-		return executeFindMultiInternal(new FindCallback(collectionPreparer, mappedQuery, mappedFields), preparer,
-				objectCallback, collectionName);
+		return executeFindMultiInternal(new FindCallback(collectionPreparer, mappedQuery, mappedFields),
+				preparer != null ? preparer : FindPublisherPreparer.NO_OP_PREPARER, objectCallback, collectionName);
 	}
 
 	CollectionPreparer<MongoCollection<Document>> createCollectionPreparer(Query query) {
@@ -2407,11 +2481,12 @@ CollectionPreparer<MongoCollection<Document>> createCollectionPreparer(Query que
 	 *
 	 * @since 2.0
 	 */
-	<S, T> Flux<T> doFind(String collectionName, CollectionPreparer<MongoCollection<Document>> collectionPreparer,
-			Document query, Document fields, Class<S> sourceClass, Class<T> targetClass, FindPublisherPreparer preparer) {
+	<T, R> Flux<R> doFind(String collectionName, CollectionPreparer<MongoCollection<Document>> collectionPreparer,
+			Document query, Document fields, Class<?> sourceClass, Class<T> targetClass,
+			QueryResultConverter<? super T, ? extends R> resultConverter, FindPublisherPreparer preparer) {
 
 		MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(sourceClass);
-		EntityProjection<T, S> projection = operations.introspectProjection(targetClass, sourceClass);
+		EntityProjection<T, ?> projection = operations.introspectProjection(targetClass, sourceClass);
 
 		QueryContext queryContext = queryOperations.createQueryContext(new BasicQuery(query, fields));
 		Document mappedFields = queryContext.getMappedFields(entity, projection);
@@ -2423,7 +2498,7 @@ <S, T> Flux<T> doFind(String collectionName, CollectionPreparer<MongoCollection<
 		}
 
 		return executeFindMultiInternal(new FindCallback(collectionPreparer, mappedQuery, mappedFields), preparer,
-				new ProjectingReadCallback<>(mongoConverter, projection, collectionName), collectionName);
+				getResultReader(projection, collectionName, resultConverter), collectionName);
 	}
 
 	protected CreateCollectionOptions convertToCreateCollectionOptions(@Nullable CollectionOptions collectionOptions) {
@@ -2448,8 +2523,8 @@ protected CreateCollectionOptions convertToCreateCollectionOptions(@Nullable Col
 	 * @return the List of converted objects.
 	 */
 	protected <T> Mono<T> doFindAndRemove(String collectionName,
-			CollectionPreparer<MongoCollection<Document>> collectionPreparer, Document query, Document fields, Document sort,
-			@Nullable Collation collation, Class<T> entityClass) {
+			CollectionPreparer<MongoCollection<Document>> collectionPreparer, Document query, Document fields,
+			@Nullable Document sort, @Nullable Collation collation, Class<T> entityClass) {
 
 		if (LOGGER.isDebugEnabled()) {
 			LOGGER.debug(String.format("findAndRemove using query: %s fields: %s sort: %s for class: %s in collection: %s",
@@ -2463,9 +2538,10 @@ protected <T> Mono<T> doFindAndRemove(String collectionName,
 				new ReadDocumentCallback<>(this.mongoConverter, entityClass, collectionName), collectionName);
 	}
 
-	protected <T> Mono<T> doFindAndModify(String collectionName,
-			CollectionPreparer<MongoCollection<Document>> collectionPreparer, Document query, Document fields, Document sort,
-			Class<T> entityClass, UpdateDefinition update, FindAndModifyOptions options) {
+	<S, T> Mono<T> doFindAndModify(String collectionName,
+			CollectionPreparer<MongoCollection<Document>> collectionPreparer, Document query, Document fields,
+			@Nullable Document sort, Class<S> entityClass, UpdateDefinition update, FindAndModifyOptions options,
+			QueryResultConverter<? super S, ? extends T> resultConverter) {
 
 		MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(entityClass);
 		UpdateContext updateContext = queryOperations.updateSingleContext(update, query, false);
@@ -2481,14 +2557,16 @@ protected <T> Mono<T> doFindAndModify(String collectionName,
 				LOGGER.debug(String.format(
 						"findAndModify using query: %s fields: %s sort: %s for class: %s and update: %s " + "in collection: %s",
 						serializeToJsonSafely(mappedQuery), fields, serializeToJsonSafely(sort), entityClass,
-						serializeToJsonSafely(mappedUpdate),
-						collectionName));
+						serializeToJsonSafely(mappedUpdate), collectionName));
 			}
 
+			EntityProjection<S, ?> projection = EntityProjection.nonProjecting(entityClass);
+			DocumentCallback<T> callback = getResultReader(projection, collectionName, resultConverter);
+
 			return executeFindOneInternal(
 					new FindAndModifyCallback(collectionPreparer, mappedQuery, fields, sort, mappedUpdate,
 							update.getArrayFilters().stream().map(ArrayFilter::asDocument).collect(Collectors.toList()), options),
-					new ReadDocumentCallback<>(this.mongoConverter, entityClass, collectionName), collectionName);
+					callback, collectionName);
 		});
 	}
 
@@ -2517,7 +2595,7 @@ protected <T> Mono<T> doFindAndReplace(String collectionName,
 		EntityProjection<T, ?> projection = operations.introspectProjection(resultType, entityType);
 
 		return doFindAndReplace(collectionName, collectionPreparer, mappedQuery, mappedFields, mappedSort, collation,
-				entityType, replacement, options, projection);
+				entityType, replacement, options, projection, QueryResultConverter.entity());
 	}
 
 	/**
@@ -2537,10 +2615,11 @@ protected <T> Mono<T> doFindAndReplace(String collectionName,
 	 *         {@literal false} and {@link FindAndReplaceOptions#isUpsert() upsert} is {@literal false}.
 	 * @since 3.4
 	 */
-	private <T> Mono<T> doFindAndReplace(String collectionName,
+	private <S, T> Mono<T> doFindAndReplace(String collectionName,
 			CollectionPreparer<MongoCollection<Document>> collectionPreparer, Document mappedQuery, Document mappedFields,
 			Document mappedSort, com.mongodb.client.model.Collation collation, Class<?> entityType, Document replacement,
-			FindAndReplaceOptions options, EntityProjection<T, ?> projection) {
+			FindAndReplaceOptions options, EntityProjection<S, ?> projection,
+			QueryResultConverter<? super S, ? extends T> resultConverter) {
 
 		return Mono.defer(() -> {
 
@@ -2552,9 +2631,10 @@ private <T> Mono<T> doFindAndReplace(String collectionName,
 						serializeToJsonSafely(replacement), collectionName));
 			}
 
+			DocumentCallback<T> resultReader = getResultReader(projection, collectionName, resultConverter);
+
 			return executeFindOneInternal(new FindAndReplaceCallback(collectionPreparer, mappedQuery, mappedFields,
-					mappedSort, replacement, collation, options),
-					new ProjectingReadCallback<>(this.mongoConverter, projection, collectionName), collectionName);
+					mappedSort, replacement, collation, options), resultReader, collectionName);
 
 		});
 	}
@@ -2659,8 +2739,7 @@ protected MongoDatabase prepareDatabase(MongoDatabase database) {
 	 * @see #setWriteConcern(WriteConcern)
 	 * @see #setWriteConcernResolver(WriteConcernResolver)
 	 */
-	@Nullable
-	protected WriteConcern prepareWriteConcern(MongoAction mongoAction) {
+	protected @Nullable WriteConcern prepareWriteConcern(MongoAction mongoAction) {
 
 		WriteConcern wc = writeConcernResolver.resolve(mongoAction);
 		return potentiallyForceAcknowledgedWrite(wc);
@@ -2679,7 +2758,7 @@ private WriteConcern potentiallyForceAcknowledgedWrite(@Nullable WriteConcern wc
 
 		if (ObjectUtils.nullSafeEquals(WriteResultChecking.EXCEPTION, writeResultChecking)) {
 			if (wc == null || wc.getWObject() == null
-					|| (wc.getWObject()instanceof Number concern && concern.intValue() < 1)) {
+					|| (wc.getWObject() instanceof Number concern && concern.intValue() < 1)) {
 				return WriteConcern.ACKNOWLEDGED;
 			}
 		}
@@ -2725,7 +2804,7 @@ private <T> Mono<T> executeFindOneInternal(ReactiveCollectionCallback<Document>
 	 * @return
 	 */
 	private <T> Flux<T> executeFindMultiInternal(ReactiveCollectionQueryCallback<Document> collectionCallback,
-			@Nullable FindPublisherPreparer preparer, DocumentCallback<T> objectCallback, String collectionName) {
+			FindPublisherPreparer preparer, DocumentCallback<T> objectCallback, String collectionName) {
 
 		return createFlux(collectionName, collection -> {
 			return Flux.from(preparer.initiateFind(collection, collectionCallback::doInCollection))
@@ -2733,6 +2812,16 @@ private <T> Flux<T> executeFindMultiInternal(ReactiveCollectionQueryCallback<Doc
 		});
 	}
 
+	@SuppressWarnings("unchecked")
+	private <T, R> DocumentCallback<R> getResultReader(EntityProjection<T, ?> projection, String collectionName,
+			QueryResultConverter<? super T, ? extends R> resultConverter) {
+
+		DocumentCallback<T> readCallback = new ProjectingReadCallback<>(mongoConverter, projection, collectionName);
+
+		return resultConverter == QueryResultConverter.entity() ? (DocumentCallback<R>) readCallback
+				: new QueryResultConverterCallback<>(resultConverter, readCallback);
+	}
+
 	/**
 	 * Exception translation {@link Function} intended for {@link Flux#onErrorMap(Function)} usage.
 	 *
@@ -2764,8 +2853,7 @@ private static RuntimeException potentiallyConvertRuntimeException(RuntimeExcept
 		return resolved == null ? ex : resolved;
 	}
 
-	@Nullable
-	private MongoPersistentEntity<?> getPersistentEntity(@Nullable Class<?> type) {
+	private @Nullable MongoPersistentEntity<?> getPersistentEntity(@Nullable Class<?> type) {
 		return type == null ? null : mappingContext.getPersistentEntity(type);
 	}
 
@@ -2785,8 +2873,8 @@ private MappingMongoConverter getDefaultMongoConverter() {
 		return converter;
 	}
 
-	@Nullable
-	private Document getMappedSortObject(Query query, Class<?> type) {
+	@Contract("null, _ -> null")
+	private @Nullable Document getMappedSortObject(@Nullable Query query, Class<?> type) {
 
 		if (query == null) {
 			return null;
@@ -2795,8 +2883,8 @@ private Document getMappedSortObject(Query query, Class<?> type) {
 		return getMappedSortObject(query.getSortObject(), type);
 	}
 
-	@Nullable
-	private Document getMappedSortObject(Document sortObject, Class<?> type) {
+	@Contract("null, _ -> null")
+	private @Nullable Document getMappedSortObject(@Nullable Document sortObject, Class<?> type) {
 
 		if (ObjectUtils.isEmpty(sortObject)) {
 			return null;
@@ -2862,7 +2950,8 @@ private static class FindCallback implements ReactiveCollectionQueryCallback<Doc
 			this(collectionPreparer, query, null);
 		}
 
-		FindCallback(CollectionPreparer<MongoCollection<Document>> collectionPreparer, Document query, Document fields) {
+		FindCallback(CollectionPreparer<MongoCollection<Document>> collectionPreparer, @Nullable Document query,
+				@Nullable Document fields) {
 			this.collectionPreparer = collectionPreparer;
 			this.query = query;
 			this.fields = fields;
@@ -2898,11 +2987,11 @@ private static class FindAndRemoveCallback implements ReactiveCollectionCallback
 		private final CollectionPreparer<MongoCollection<Document>> collectionPreparer;
 		private final Document query;
 		private final Document fields;
-		private final Document sort;
+		private final @Nullable Document sort;
 		private final Optional<Collation> collation;
 
 		FindAndRemoveCallback(CollectionPreparer<MongoCollection<Document>> collectionPreparer, Document query,
-				Document fields, Document sort, @Nullable Collation collation) {
+				Document fields, @Nullable Document sort, @Nullable Collation collation) {
 			this.collectionPreparer = collectionPreparer;
 			this.query = query;
 			this.fields = fields;
@@ -2928,14 +3017,15 @@ private static class FindAndModifyCallback implements ReactiveCollectionCallback
 
 		private final CollectionPreparer<MongoCollection<Document>> collectionPreparer;
 		private final Document query;
-		private final Document fields;
-		private final Document sort;
+		private final @Nullable Document fields;
+		private final @Nullable Document sort;
 		private final Object update;
 		private final List<Document> arrayFilters;
 		private final FindAndModifyOptions options;
 
 		FindAndModifyCallback(CollectionPreparer<MongoCollection<Document>> collectionPreparer, Document query,
-				Document fields, Document sort, Object update, List<Document> arrayFilters, FindAndModifyOptions options) {
+				@Nullable Document fields, @Nullable Document sort, Object update, List<Document> arrayFilters,
+				FindAndModifyOptions options) {
 
 			this.collectionPreparer = collectionPreparer;
 			this.query = query;
@@ -2973,7 +3063,7 @@ public Publisher<Document> doInCollection(MongoCollection<Document> collection)
 		}
 
 		private static FindOneAndUpdateOptions convertToFindOneAndUpdateOptions(FindAndModifyOptions options,
-				Document fields, Document sort, List<Document> arrayFilters) {
+				@Nullable Document fields, @Nullable Document sort, List<Document> arrayFilters) {
 
 			FindOneAndUpdateOptions result = new FindOneAndUpdateOptions();
 
@@ -3009,11 +3099,11 @@ private static class FindAndReplaceCallback implements ReactiveCollectionCallbac
 		private final Document fields;
 		private final Document sort;
 		private final Document update;
-		private final @Nullable com.mongodb.client.model.Collation collation;
+		private final com.mongodb.client.model.@Nullable Collation collation;
 		private final FindAndReplaceOptions options;
 
 		FindAndReplaceCallback(CollectionPreparer<MongoCollection<Document>> collectionPreparer, Document query,
-				Document fields, Document sort, Document update, com.mongodb.client.model.Collation collation,
+				Document fields, Document sort, Document update, com.mongodb.client.model.@Nullable Collation collation,
 				FindAndReplaceOptions options) {
 			this.collectionPreparer = collectionPreparer;
 			this.query = query;
@@ -3049,7 +3139,8 @@ private FindOneAndReplaceOptions convertToFindOneAndReplaceOptions(FindAndReplac
 		}
 	}
 
-	private static FindOneAndDeleteOptions convertToFindOneAndDeleteOptions(Document fields, Document sort) {
+	private static FindOneAndDeleteOptions convertToFindOneAndDeleteOptions(@Nullable Document fields,
+			@Nullable Document sort) {
 
 		FindOneAndDeleteOptions result = new FindOneAndDeleteOptions();
 		result = result.projection(fields).sort(sort);
@@ -3090,6 +3181,22 @@ interface ReactiveCollectionQueryCallback<T> extends ReactiveCollectionCallback<
 		FindPublisher<T> doInCollection(MongoCollection<Document> collection) throws MongoException, DataAccessException;
 	}
 
+	static class QueryResultConverterCallback<T, R> implements DocumentCallback<R> {
+
+		private final QueryResultConverter<? super T, ? extends R> converter;
+		private final DocumentCallback<T> delegate;
+
+		QueryResultConverterCallback(QueryResultConverter<? super T, ? extends R> converter, DocumentCallback<T> delegate) {
+			this.converter = converter;
+			this.delegate = delegate;
+		}
+
+		@Override
+		public Mono<R> doWith(Document object) {
+			return delegate.doWith(object).map(it -> converter.mapDocument(object, () -> it));
+		}
+	}
+
 	/**
 	 * Simple {@link DocumentCallback} that will transform {@link Document} into the given target type using the given
 	 * {@link EntityReader}.
@@ -3206,7 +3313,7 @@ public Mono<GeoResult<T>> doWith(Document object) {
 
 			double distance = getDistance(object);
 
-			return delegate.doWith(object).map(doWith -> new GeoResult<>(doWith, new Distance(distance, metric)));
+			return delegate.doWith(object).map(doWith -> new GeoResult<>(doWith, Distance.of(distance, metric)));
 		}
 
 		double getDistance(Document object) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperation.java
index 378f13d917..dd515cb37c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperation.java
@@ -20,6 +20,7 @@
 
 import org.springframework.data.mongodb.core.query.CriteriaDefinition;
 import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.lang.Contract;
 
 import com.mongodb.client.result.DeleteResult;
 
@@ -56,16 +57,22 @@ public interface ReactiveRemoveOperation {
 	<T> ReactiveRemove<T> remove(Class<T> domainType);
 
 	/**
-	 * Compose remove execution by calling one of the terminating methods.
+	 * @author Christoph Strobl
+	 * @since 5.0
 	 */
-	interface TerminatingRemove<T> {
+	interface TerminatingResults<T> {
 
 		/**
-		 * Remove all documents matching.
+		 * Map the query result to a different type using {@link QueryResultConverter}.
 		 *
-		 * @return {@link Mono} emitting the {@link DeleteResult}. Never {@literal null}.
+		 * @param <R> {@link Class type} of the result.
+		 * @param converter the converter, must not be {@literal null}.
+		 * @return new instance of {@link ExecutableFindOperation.TerminatingResults}.
+		 * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}.
+		 * @since 5.0
 		 */
-		Mono<DeleteResult> all();
+		@Contract("_ -> new")
+		<R> TerminatingResults<R> map(QueryResultConverter<? super T, ? extends R> converter);
 
 		/**
 		 * Remove and return all matching documents. <br/>
@@ -78,6 +85,20 @@ interface TerminatingRemove<T> {
 		Flux<T> findAndRemove();
 	}
 
+	/**
+	 * Compose remove execution by calling one of the terminating methods.
+	 */
+	interface TerminatingRemove<T> extends TerminatingResults<T> {
+
+		/**
+		 * Remove all documents matching.
+		 *
+		 * @return {@link Mono} emitting the {@link DeleteResult}. Never {@literal null}.
+		 */
+		Mono<DeleteResult> all();
+
+	}
+
 	/**
 	 * Collection override (optional).
 	 */
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupport.java
index 97c9cb0d0e..f77b5296d7 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupport.java
@@ -18,6 +18,7 @@
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
@@ -46,22 +47,25 @@ public <T> ReactiveRemove<T> remove(Class<T> domainType) {
 
 		Assert.notNull(domainType, "DomainType must not be null");
 
-		return new ReactiveRemoveSupport<>(template, domainType, ALL_QUERY, null);
+		return new ReactiveRemoveSupport<>(template, domainType, ALL_QUERY, null, QueryResultConverter.entity());
 	}
 
-	static class ReactiveRemoveSupport<T> implements ReactiveRemove<T>, RemoveWithCollection<T> {
+	static class ReactiveRemoveSupport<S, T> implements ReactiveRemove<T>, RemoveWithCollection<T> {
 
 		private final ReactiveMongoTemplate template;
-		private final Class<T> domainType;
+		private final Class<S> domainType;
 		private final Query query;
-		private final String collection;
+		private final @Nullable String collection;
+		private final QueryResultConverter<? super S, ? extends T> resultConverter;
 
-		ReactiveRemoveSupport(ReactiveMongoTemplate template, Class<T> domainType, Query query, String collection) {
+		ReactiveRemoveSupport(ReactiveMongoTemplate template, Class<S> domainType, Query query, @Nullable String collection,
+				QueryResultConverter<? super S, ? extends T> resultConverter) {
 
 			this.template = template;
 			this.domainType = domainType;
 			this.query = query;
 			this.collection = collection;
+			this.resultConverter = resultConverter;
 		}
 
 		@Override
@@ -69,7 +73,7 @@ public RemoveWithQuery<T> inCollection(String collection) {
 
 			Assert.hasText(collection, "Collection must not be null nor empty");
 
-			return new ReactiveRemoveSupport<>(template, domainType, query, collection);
+			return new ReactiveRemoveSupport<>(template, domainType, query, collection, resultConverter);
 		}
 
 		@Override
@@ -77,7 +81,7 @@ public TerminatingRemove<T> matching(Query query) {
 
 			Assert.notNull(query, "Query must not be null");
 
-			return new ReactiveRemoveSupport<>(template, domainType, query, collection);
+			return new ReactiveRemoveSupport<>(template, domainType, query, collection, resultConverter);
 		}
 
 		@Override
@@ -93,7 +97,13 @@ public Flux<T> findAndRemove() {
 
 			String collectionName = getCollectionName();
 
-			return template.doFindAndDelete(collectionName, query, domainType);
+			return template.doFindAndDelete(collectionName, query, domainType, resultConverter);
+		}
+
+		@Override
+		@SuppressWarnings({ "unchecked", "rawtypes" })
+		public <R> TerminatingResults<R> map(QueryResultConverter<? super T, ? extends R> converter) {
+			return new ReactiveRemoveSupport<>(template, (Class) domainType, query, collection, converter);
 		}
 
 		private String getCollectionName() {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperation.java
index 51f75f3265..c9f92029cc 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperation.java
@@ -15,6 +15,7 @@
  */
 package org.springframework.data.mongodb.core;
 
+import org.jetbrains.annotations.Contract;
 import reactor.core.publisher.Mono;
 
 import org.springframework.data.mongodb.core.aggregation.AggregationUpdate;
@@ -64,6 +65,18 @@ public interface ReactiveUpdateOperation {
 	 */
 	interface TerminatingFindAndModify<T> {
 
+		/**
+		 * Map the query result to a different type using {@link QueryResultConverter}.
+		 *
+		 * @param <R> {@link Class type} of the result.
+		 * @param converter the converter, must not be {@literal null}.
+		 * @return new instance of {@link TerminatingFindAndModify}.
+		 * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}.
+		 * @since 5.0
+		 */
+		@Contract("_ -> new")
+		<R> TerminatingFindAndModify<R> map(QueryResultConverter<? super T, ? extends R> converter);
+
 		/**
 		 * Find, modify and return the first matching document.
 		 *
@@ -97,6 +110,18 @@ interface TerminatingReplace {
 	 */
 	interface TerminatingFindAndReplace<T> extends TerminatingReplace {
 
+		/**
+		 * Map the query result to a different type using {@link QueryResultConverter}.
+		 *
+		 * @param <R> {@link Class type} of the result.
+		 * @param converter the converter, must not be {@literal null}.
+		 * @return new instance of {@link TerminatingFindAndModify}.
+		 * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}.
+		 * @since 5.0
+		 */
+		@Contract("_ -> new")
+		<R> TerminatingFindAndReplace<R> map(QueryResultConverter<? super T, ? extends R> converter);
+
 		/**
 		 * Find, replace and return the first matching document.
 		 *
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java
index 51cd99dc93..876a7a5aa2 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java
@@ -17,9 +17,9 @@
 
 import reactor.core.publisher.Mono;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mongodb.core.query.UpdateDefinition;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 
@@ -47,26 +47,27 @@ public <T> ReactiveUpdate<T> update(Class<T> domainType) {
 
 		Assert.notNull(domainType, "DomainType must not be null");
 
-		return new ReactiveUpdateSupport<>(template, domainType, ALL_QUERY, null, null, null, null, null, domainType);
+		return new ReactiveUpdateSupport<>(template, domainType, ALL_QUERY, null, null, null, null, null, domainType, QueryResultConverter.entity());
 	}
 
-	static class ReactiveUpdateSupport<T>
+	static class ReactiveUpdateSupport<S, T>
 			implements ReactiveUpdate<T>, UpdateWithCollection<T>, UpdateWithQuery<T>, TerminatingUpdate<T>,
 			FindAndReplaceWithOptions<T>, FindAndReplaceWithProjection<T>, TerminatingFindAndReplace<T> {
 
 		private final ReactiveMongoTemplate template;
 		private final Class<?> domainType;
 		private final Query query;
-		private final org.springframework.data.mongodb.core.query.UpdateDefinition update;
-		@Nullable private final String collection;
-		@Nullable private final FindAndModifyOptions findAndModifyOptions;
-		@Nullable private final FindAndReplaceOptions findAndReplaceOptions;
-		@Nullable private final Object replacement;
-		private final Class<T> targetType;
-
-		ReactiveUpdateSupport(ReactiveMongoTemplate template, Class<?> domainType, Query query, UpdateDefinition update,
-				String collection, FindAndModifyOptions findAndModifyOptions, FindAndReplaceOptions findAndReplaceOptions,
-				Object replacement, Class<T> targetType) {
+		private final org.springframework.data.mongodb.core.query.@Nullable UpdateDefinition update;
+		private final @Nullable String collection;
+		private final @Nullable FindAndModifyOptions findAndModifyOptions;
+		private final @Nullable FindAndReplaceOptions findAndReplaceOptions;
+		private final @Nullable Object replacement;
+		private final Class<S> targetType;
+		private final QueryResultConverter<? super S, ? extends T> resultConverter;
+
+		ReactiveUpdateSupport(ReactiveMongoTemplate template, Class<?> domainType, Query query, @Nullable UpdateDefinition update,
+			@Nullable String collection, @Nullable FindAndModifyOptions findAndModifyOptions, @Nullable FindAndReplaceOptions findAndReplaceOptions,
+			@Nullable Object replacement, Class<S> targetType, QueryResultConverter<? super S, ? extends T> resultConverter) {
 
 			this.template = template;
 			this.domainType = domainType;
@@ -77,6 +78,7 @@ static class ReactiveUpdateSupport<T>
 			this.findAndReplaceOptions = findAndReplaceOptions;
 			this.replacement = replacement;
 			this.targetType = targetType;
+			this.resultConverter = resultConverter;
 		}
 
 		@Override
@@ -85,7 +87,7 @@ public TerminatingUpdate<T> apply(org.springframework.data.mongodb.core.query.Up
 			Assert.notNull(update, "Update must not be null");
 
 			return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
-					findAndReplaceOptions, replacement, targetType);
+					findAndReplaceOptions, replacement, targetType, resultConverter);
 		}
 
 		@Override
@@ -94,7 +96,7 @@ public UpdateWithQuery<T> inCollection(String collection) {
 			Assert.hasText(collection, "Collection must not be null nor empty");
 
 			return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
-					findAndReplaceOptions, replacement, targetType);
+					findAndReplaceOptions, replacement, targetType, resultConverter);
 		}
 
 		@Override
@@ -108,20 +110,25 @@ public Mono<UpdateResult> upsert() {
 		}
 
 		@Override
+		@SuppressWarnings({"unchecked", "rawtypes", "NullAway"})
 		public Mono<T> findAndModify() {
 
 			String collectionName = getCollectionName();
 
 			return template.findAndModify(query, update,
-					findAndModifyOptions != null ? findAndModifyOptions : FindAndModifyOptions.none(), targetType,
-					collectionName);
+					findAndModifyOptions != null ? findAndModifyOptions : FindAndModifyOptions.none(), (Class) targetType,
+					collectionName, resultConverter);
 		}
 
 		@Override
+		@SuppressWarnings({"unchecked","rawtypes"})
 		public Mono<T> findAndReplace() {
+
+			Assert.notNull(replacement, "Replacement must be set first");
+
 			return template.findAndReplace(query, replacement,
 					findAndReplaceOptions != null ? findAndReplaceOptions : FindAndReplaceOptions.none(), (Class) domainType,
-					getCollectionName(), targetType);
+					getCollectionName(), targetType, resultConverter);
 		}
 
 		@Override
@@ -130,7 +137,7 @@ public UpdateWithUpdate<T> matching(Query query) {
 			Assert.notNull(query, "Query must not be null");
 
 			return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
-					findAndReplaceOptions, replacement, targetType);
+					findAndReplaceOptions, replacement, targetType, resultConverter);
 		}
 
 		@Override
@@ -144,7 +151,7 @@ public TerminatingFindAndModify<T> withOptions(FindAndModifyOptions options) {
 			Assert.notNull(options, "Options must not be null");
 
 			return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, options,
-					findAndReplaceOptions, replacement, targetType);
+					findAndReplaceOptions, replacement, targetType, resultConverter);
 		}
 
 		@Override
@@ -153,7 +160,7 @@ public FindAndReplaceWithProjection<T> replaceWith(T replacement) {
 			Assert.notNull(replacement, "Replacement must not be null");
 
 			return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
-					findAndReplaceOptions, replacement, targetType);
+					findAndReplaceOptions, replacement, targetType, resultConverter);
 		}
 
 		@Override
@@ -162,7 +169,7 @@ public FindAndReplaceWithProjection<T> withOptions(FindAndReplaceOptions options
 			Assert.notNull(options, "Options must not be null");
 
 			return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, options,
-					replacement, targetType);
+					replacement, targetType, resultConverter);
 		}
 
 		@Override
@@ -173,7 +180,7 @@ public TerminatingReplace withOptions(ReplaceOptions options) {
 				target.upsert();
 			}
 			return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
-					target, replacement, targetType);
+					target, replacement, targetType, resultConverter);
 		}
 
 		@Override
@@ -182,10 +189,17 @@ public <R> FindAndReplaceWithOptions<R> as(Class<R> resultType) {
 			Assert.notNull(resultType, "ResultType must not be null");
 
 			return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
-					findAndReplaceOptions, replacement, resultType);
+					findAndReplaceOptions, replacement, resultType, QueryResultConverter.entity());
+		}
+
+		@Override
+		public <R> ReactiveUpdateSupport<S, R> map(QueryResultConverter<? super T, ? extends R> converter) {
+			return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
+				findAndReplaceOptions, replacement, targetType, this.resultConverter.andThen(converter));
 		}
 
 		@Override
+		@SuppressWarnings("NullAway")
 		public Mono <UpdateResult> replaceFirst() {
 
 			if (replacement != null) {
@@ -197,6 +211,7 @@ public Mono <UpdateResult> replaceFirst() {
 					findAndReplaceOptions != null ? findAndReplaceOptions : ReplaceOptions.none(), getCollectionName());
 		}
 
+		@SuppressWarnings("NullAway")
 		private Mono<UpdateResult> doUpdate(boolean multi, boolean upsert) {
 			return template.doUpdate(getCollectionName(), query, update, domainType, upsert, multi);
 		}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadConcernAware.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadConcernAware.java
index 00c5815fc9..7a7e5fdfb2 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadConcernAware.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadConcernAware.java
@@ -15,7 +15,7 @@
  */
 package org.springframework.data.mongodb.core;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 import com.mongodb.ReadConcern;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadPreferenceAware.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadPreferenceAware.java
index 74bca9abea..e6f3fc0daf 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadPreferenceAware.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadPreferenceAware.java
@@ -15,7 +15,7 @@
  */
 package org.springframework.data.mongodb.core;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 import com.mongodb.ReadPreference;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReplaceOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReplaceOptions.java
index a2e2ba24c0..a487cde669 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReplaceOptions.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReplaceOptions.java
@@ -16,6 +16,7 @@
 package org.springframework.data.mongodb.core;
 
 import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.lang.Contract;
 
 /**
  * Options for {@link org.springframework.data.mongodb.core.MongoOperations#replace(Query, Object) replace operations}. Defaults to
@@ -69,6 +70,7 @@ public static ReplaceOptions none() {
 	 *
 	 * @return this.
 	 */
+	@Contract("-> this")
 	public ReplaceOptions upsert() {
 
 		this.upsert = true;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScriptOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScriptOperations.java
index a01760368a..2ec71b415a 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScriptOperations.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScriptOperations.java
@@ -17,9 +17,9 @@
 
 import java.util.Set;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.script.ExecutableMongoScript;
 import org.springframework.data.mongodb.core.script.NamedMongoScript;
-import org.springframework.lang.Nullable;
 
 
 /**
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScrollUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScrollUtils.java
index 85ddce7656..62e6d6c513 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScrollUtils.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScrollUtils.java
@@ -29,6 +29,7 @@
 import org.springframework.data.domain.Window;
 import org.springframework.data.mongodb.core.EntityOperations.Entity;
 import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.util.Assert;
 
 /**
  * Utilities to run scroll queries and create {@link Window} results.
@@ -48,7 +49,11 @@ class ScrollUtils {
 	 */
 	static KeysetScrollQuery createKeysetPaginationQuery(Query query, String idPropertyName) {
 
+
 		KeysetScrollPosition keyset = query.getKeyset();
+
+		Assert.notNull(keyset, "Query.keyset must not be null");
+
 		KeysetScrollDirector director = KeysetScrollDirector.of(keyset.getDirection());
 		Document sortObject = director.getSortObject(idPropertyName, query);
 		Document fieldsObject = director.getFieldsObject(query.getFieldsObject(), sortObject);
@@ -61,6 +66,9 @@ static <T> Window<T> createWindow(Query query, List<T> result, Class<?> sourceTy
 
 		Document sortObject = query.getSortObject();
 		KeysetScrollPosition keyset = query.getKeyset();
+
+		Assert.notNull(keyset, "Query.keyset must not be null");
+
 		Direction direction = keyset.getDirection();
 		KeysetScrollDirector director = KeysetScrollDirector.of(direction);
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionCallback.java
index 55a87ecadf..76a6d525f8 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionCallback.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionCallback.java
@@ -15,8 +15,8 @@
  */
 package org.springframework.data.mongodb.core;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.query.Query;
-import org.springframework.lang.Nullable;
 
 /**
  * Callback interface for executing operations within a {@link com.mongodb.session.ClientSession}.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionScoped.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionScoped.java
index 33ad9d7318..906d682685 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionScoped.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionScoped.java
@@ -17,10 +17,10 @@
 
 import java.util.function.Consumer;
 
-import org.springframework.lang.Nullable;
-
 import com.mongodb.client.ClientSession;
 
+import org.jspecify.annotations.Nullable;
+
 /**
  * Gateway interface to execute {@link ClientSession} bound operations against MongoDB via a {@link SessionCallback}.
  * <br />
@@ -42,8 +42,7 @@ public interface SessionScoped {
 	 * @param <T> return type.
 	 * @return a result object returned by the action. Can be {@literal null}.
 	 */
-	@Nullable
-	default <T> T execute(SessionCallback<T> action) {
+	default <T> @Nullable T execute(SessionCallback<T> action) {
 		return execute(action, session -> {});
 	}
 
@@ -60,6 +59,5 @@ default <T> T execute(SessionCallback<T> action) {
 	 * @param <T> return type.
 	 * @return a result object returned by the action. Can be {@literal null}.
 	 */
-	@Nullable
-	<T> T execute(SessionCallback<T> action, Consumer<ClientSession> doFinally);
+	<T> @Nullable T execute(SessionCallback<T> action, Consumer<ClientSession> doFinally);
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java
index 84edf13d57..529f912e6c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java
@@ -18,13 +18,13 @@
 import reactor.core.publisher.Mono;
 
 import org.bson.codecs.configuration.CodecRegistry;
+import org.jspecify.annotations.Nullable;
 import org.springframework.aop.framework.ProxyFactory;
 import org.springframework.beans.factory.DisposableBean;
 import org.springframework.dao.DataAccessException;
 import org.springframework.dao.support.PersistenceExceptionTranslator;
 import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory;
 import org.springframework.data.mongodb.SessionAwareMethodInterceptor;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SortingQueryCursorPreparer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SortingQueryCursorPreparer.java
index c69fb4ad15..1652dca259 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SortingQueryCursorPreparer.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SortingQueryCursorPreparer.java
@@ -16,7 +16,7 @@
 package org.springframework.data.mongodb.core;
 
 import org.bson.Document;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /**
  * {@link CursorPreparer} that exposes its {@link Document sort document}.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ViewOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ViewOptions.java
index e50e1088cb..b4b525fc97 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ViewOptions.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ViewOptions.java
@@ -17,8 +17,9 @@
 
 import java.util.Optional;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.query.Collation;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 
 /**
  * Immutable object holding additional options to be applied when creating a MongoDB
@@ -59,6 +60,7 @@ public Optional<Collation> getCollation() {
 	 * @param collation the {@link Collation} to use for language-specific string comparison.
 	 * @return new instance of {@link ViewOptions}.
 	 */
+	@Contract("_ -> new")
 	public ViewOptions collation(Collation collation) {
 		return new ViewOptions(collation);
 	}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernAware.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernAware.java
index d6e4119b20..bdc7de6663 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernAware.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernAware.java
@@ -15,7 +15,7 @@
  */
 package org.springframework.data.mongodb.core;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 import com.mongodb.WriteConcern;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernResolver.java
index 8df4171844..a72c656e47 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernResolver.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernResolver.java
@@ -15,7 +15,7 @@
  */
 package org.springframework.data.mongodb.core;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 import com.mongodb.WriteConcern;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java
index d4cdece411..710b570ed7 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java
@@ -26,6 +26,7 @@
 
 import org.bson.Document;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.domain.Sort;
 import org.springframework.data.domain.Sort.Order;
 import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
@@ -282,7 +283,7 @@ protected <T> T get(int index) {
 	 * @since 2.1
 	 */
 	@SuppressWarnings("unchecked")
-	protected <T> T get(Object key) {
+	protected <T> @Nullable T get(Object key) {
 
 		Assert.isInstanceOf(Map.class, this.value, "Value must be a type of Map");
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperators.java
index cf6485c230..fa44656c99 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperators.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperators.java
@@ -22,6 +22,8 @@
 import java.util.Map;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 
 /**
@@ -60,8 +62,8 @@ public static AccumulatorOperatorFactory valueOf(AggregationExpression expressio
 	 */
 	public static class AccumulatorOperatorFactory {
 
-		private final String fieldReference;
-		private final AggregationExpression expression;
+		private final @Nullable String fieldReference;
+		private final @Nullable AggregationExpression expression;
 
 		/**
 		 * Creates new {@link AccumulatorOperatorFactory} for given {@literal fieldReference}.
@@ -93,6 +95,7 @@ public AccumulatorOperatorFactory(AggregationExpression expression) {
 		 *
 		 * @return new instance of {@link Sum}.
 		 */
+		@SuppressWarnings("NullAway")
 		public Sum sum() {
 			return usesFieldRef() ? Sum.sumOf(fieldReference) : Sum.sumOf(expression);
 		}
@@ -103,6 +106,7 @@ public Sum sum() {
 		 *
 		 * @return new instance of {@link Avg}.
 		 */
+		@SuppressWarnings("NullAway")
 		public Avg avg() {
 			return usesFieldRef() ? Avg.avgOf(fieldReference) : Avg.avgOf(expression);
 		}
@@ -113,6 +117,7 @@ public Avg avg() {
 		 *
 		 * @return new instance of {@link Max}.
 		 */
+		@SuppressWarnings("NullAway")
 		public Max max() {
 			return usesFieldRef() ? Max.maxOf(fieldReference) : Max.maxOf(expression);
 		}
@@ -134,6 +139,7 @@ public Max max(int numberOfResults) {
 		 *
 		 * @return new instance of {@link Min}.
 		 */
+		@SuppressWarnings("NullAway")
 		public Min min() {
 			return usesFieldRef() ? Min.minOf(fieldReference) : Min.minOf(expression);
 		}
@@ -155,6 +161,7 @@ public Min min(int numberOfResults) {
 		 *
 		 * @return new instance of {@link StdDevPop}.
 		 */
+		@SuppressWarnings("NullAway")
 		public StdDevPop stdDevPop() {
 			return usesFieldRef() ? StdDevPop.stdDevPopOf(fieldReference) : StdDevPop.stdDevPopOf(expression);
 		}
@@ -165,6 +172,7 @@ public StdDevPop stdDevPop() {
 		 *
 		 * @return new instance of {@link StdDevSamp}.
 		 */
+		@SuppressWarnings("NullAway")
 		public StdDevSamp stdDevSamp() {
 			return usesFieldRef() ? StdDevSamp.stdDevSampOf(fieldReference) : StdDevSamp.stdDevSampOf(expression);
 		}
@@ -193,6 +201,7 @@ public CovariancePop covariancePop(AggregationExpression expression) {
 			return covariancePop().and(expression);
 		}
 
+		@SuppressWarnings("NullAway")
 		private CovariancePop covariancePop() {
 			return usesFieldRef() ? CovariancePop.covariancePopOf(fieldReference) : CovariancePop.covariancePopOf(expression);
 		}
@@ -221,6 +230,7 @@ public CovarianceSamp covarianceSamp(AggregationExpression expression) {
 			return covarianceSamp().and(expression);
 		}
 
+		@SuppressWarnings("NullAway")
 		private CovarianceSamp covarianceSamp() {
 			return usesFieldRef() ? CovarianceSamp.covarianceSampOf(fieldReference)
 					: CovarianceSamp.covarianceSampOf(expression);
@@ -233,6 +243,7 @@ private CovarianceSamp covarianceSamp() {
 		 * @return new instance of {@link ExpMovingAvg}.
 		 * @since 3.3
 		 */
+		@SuppressWarnings("NullAway")
 		public ExpMovingAvgBuilder expMovingAvg() {
 
 			ExpMovingAvg expMovingAvg = usesFieldRef() ? ExpMovingAvg.expMovingAvgOf(fieldReference)
@@ -252,13 +263,14 @@ public ExpMovingAvg alpha(double exponentialDecayValue) {
 		}
 
 		/**
-		 * Creates new {@link AggregationExpression} that calculates the requested percentile(s) of the
-		 * associated numeric value expression.
+		 * Creates new {@link AggregationExpression} that calculates the requested percentile(s) of the associated numeric
+		 * value expression.
 		 *
 		 * @return new instance of {@link Percentile}.
 		 * @param percentages must not be {@literal null}.
 		 * @since 4.2
 		 */
+		@SuppressWarnings("NullAway")
 		public Percentile percentile(Double... percentages) {
 			Percentile percentile = usesFieldRef() ? Percentile.percentileOf(fieldReference)
 					: Percentile.percentileOf(expression);
@@ -271,6 +283,7 @@ public Percentile percentile(Double... percentages) {
 		 * @return new instance of {@link Median}.
 		 * @since 4.2
 		 */
+		@SuppressWarnings("NullAway")
 		public Median median() {
 			return usesFieldRef() ? Median.medianOf(fieldReference) : Median.medianOf(expression);
 		}
@@ -339,6 +352,7 @@ public static Sum sumOf(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Sum}.
 		 */
+		@Contract("_ -> new")
 		public static Sum sumOf(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -352,6 +366,7 @@ public static Sum sumOf(AggregationExpression expression) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link Sum}.
 		 */
+		@Contract("_ -> new")
 		public Sum and(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -365,6 +380,7 @@ public Sum and(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Sum}.
 		 */
+		@Contract("_ -> new")
 		public Sum and(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -379,6 +395,7 @@ public Sum and(AggregationExpression expression) {
 		 * @return new instance of {@link Sum}.
 		 * @since 2.2
 		 */
+		@Contract("_ -> new")
 		public Sum and(Number value) {
 
 			Assert.notNull(value, "Value must not be null");
@@ -386,7 +403,6 @@ public Sum and(Number value) {
 		}
 
 		@Override
-		@SuppressWarnings("unchecked")
 		public Document toDocument(Object value, AggregationOperationContext context) {
 
 			if (value instanceof List<?> list && list.size() == 1) {
@@ -444,6 +460,7 @@ public static Avg avgOf(AggregationExpression expression) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link Avg}.
 		 */
+		@Contract("_ -> new")
 		public Avg and(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -457,6 +474,7 @@ public Avg and(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Avg}.
 		 */
+		@Contract("_ -> new")
 		public Avg and(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -464,7 +482,6 @@ public Avg and(AggregationExpression expression) {
 		}
 
 		@Override
-		@SuppressWarnings("unchecked")
 		public Document toDocument(Object value, AggregationOperationContext context) {
 
 			if (value instanceof List<?> list && list.size() == 1) {
@@ -522,6 +539,7 @@ public static Max maxOf(AggregationExpression expression) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link Max}.
 		 */
+		@Contract("_ -> new")
 		public Max and(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -535,6 +553,7 @@ public Max and(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Max}.
 		 */
+		@Contract("_ -> new")
 		public Max and(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -548,11 +567,13 @@ public Max and(AggregationExpression expression) {
 		 * @param numberOfResults
 		 * @return new instance of {@link Max}.
 		 */
+		@Contract("_ -> new")
 		public Max limit(int numberOfResults) {
 			return new Max(append("n", numberOfResults));
 		}
 
 		@Override
+		@SuppressWarnings("NullAway")
 		public Document toDocument(AggregationOperationContext context) {
 			if (get("n") == null) {
 				return toDocument(get("input"), context);
@@ -619,6 +640,7 @@ public static Min minOf(AggregationExpression expression) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link Min}.
 		 */
+		@Contract("_ -> new")
 		public Min and(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -632,6 +654,7 @@ public Min and(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Min}.
 		 */
+		@Contract("_ -> new")
 		public Min and(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -645,11 +668,13 @@ public Min and(AggregationExpression expression) {
 		 * @param numberOfResults
 		 * @return new instance of {@link Min}.
 		 */
+		@Contract("_ -> new")
 		public Min limit(int numberOfResults) {
 			return new Min(append("n", numberOfResults));
 		}
 
 		@Override
+		@SuppressWarnings("NullAway")
 		public Document toDocument(AggregationOperationContext context) {
 
 			if (get("n") == null) {
@@ -659,7 +684,6 @@ public Document toDocument(AggregationOperationContext context) {
 		}
 
 		@Override
-		@SuppressWarnings("unchecked")
 		public Document toDocument(Object value, AggregationOperationContext context) {
 
 			if (value instanceof List<?> list && list.size() == 1) {
@@ -717,6 +741,7 @@ public static StdDevPop stdDevPopOf(AggregationExpression expression) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link StdDevPop}.
 		 */
+		@Contract("_ -> new")
 		public StdDevPop and(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -730,6 +755,7 @@ public StdDevPop and(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link StdDevPop}.
 		 */
+		@Contract("_ -> new")
 		public StdDevPop and(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -737,7 +763,6 @@ public StdDevPop and(AggregationExpression expression) {
 		}
 
 		@Override
-		@SuppressWarnings("unchecked")
 		public Document toDocument(Object value, AggregationOperationContext context) {
 
 			if (value instanceof List<?> list && list.size() == 1) {
@@ -795,6 +820,7 @@ public static StdDevSamp stdDevSampOf(AggregationExpression expression) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link StdDevSamp}.
 		 */
+		@Contract("_ -> new")
 		public StdDevSamp and(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -808,6 +834,7 @@ public StdDevSamp and(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link StdDevSamp}.
 		 */
+		@Contract("_ -> new")
 		public StdDevSamp and(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -815,7 +842,6 @@ public StdDevSamp and(AggregationExpression expression) {
 		}
 
 		@Override
-		@SuppressWarnings("unchecked")
 		public Document toDocument(Object value, AggregationOperationContext context) {
 
 			if (value instanceof List<?> list && list.size() == 1) {
@@ -866,6 +892,7 @@ public static CovariancePop covariancePopOf(AggregationExpression expression) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link CovariancePop}.
 		 */
+		@Contract("_ -> new")
 		public CovariancePop and(String fieldReference) {
 			return new CovariancePop(append(asFields(fieldReference)));
 		}
@@ -876,6 +903,7 @@ public CovariancePop and(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link CovariancePop}.
 		 */
+		@Contract("_ -> new")
 		public CovariancePop and(AggregationExpression expression) {
 			return new CovariancePop(append(expression));
 		}
@@ -926,6 +954,7 @@ public static CovarianceSamp covarianceSampOf(AggregationExpression expression)
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link CovarianceSamp}.
 		 */
+		@Contract("_ -> new")
 		public CovarianceSamp and(String fieldReference) {
 			return new CovarianceSamp(append(asFields(fieldReference)));
 		}
@@ -936,6 +965,7 @@ public CovarianceSamp and(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link CovarianceSamp}.
 		 */
+		@Contract("_ -> new")
 		public CovarianceSamp and(AggregationExpression expression) {
 			return new CovarianceSamp(append(expression));
 		}
@@ -986,6 +1016,7 @@ public static ExpMovingAvg expMovingAvgOf(AggregationExpression expression) {
 		 * @param numberOfHistoricalDocuments
 		 * @return new instance of {@link ExpMovingAvg}.
 		 */
+		@Contract("_ -> new")
 		public ExpMovingAvg n/*umber of historical documents*/(int numberOfHistoricalDocuments) {
 			return new ExpMovingAvg(append("N", numberOfHistoricalDocuments));
 		}
@@ -997,6 +1028,7 @@ public static ExpMovingAvg expMovingAvgOf(AggregationExpression expression) {
 		 * @param exponentialDecayValue
 		 * @return new instance of {@link ExpMovingAvg}.
 		 */
+		@Contract("_ -> new")
 		public ExpMovingAvg alpha(double exponentialDecayValue) {
 			return new ExpMovingAvg(append("alpha", exponentialDecayValue));
 		}
@@ -1055,6 +1087,7 @@ public static Percentile percentileOf(AggregationExpression expression) {
 		 * @param percentages must not be {@literal null}.
 		 * @return new instance of {@link Percentile}.
 		 */
+		@Contract("_ -> new")
 		public Percentile percentages(Double... percentages) {
 
 			Assert.notEmpty(percentages, "Percentages must not be null or empty");
@@ -1068,6 +1101,7 @@ public Percentile percentages(Double... percentages) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link Percentile}.
 		 */
+		@Contract("_ -> new")
 		public Percentile and(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -1081,6 +1115,7 @@ public Percentile and(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Percentile}.
 		 */
+		@Contract("_ -> new")
 		public Percentile and(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -1142,6 +1177,7 @@ public static Median medianOf(AggregationExpression expression) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link Median}.
 		 */
+		@Contract("_ -> new")
 		public Median and(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -1155,6 +1191,7 @@ public Median and(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Median}.
 		 */
+		@Contract("_ -> new")
 		public Median and(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperation.java
index 0dc1588bf8..e76ebb894d 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperation.java
@@ -19,8 +19,9 @@
 import java.util.LinkedHashMap;
 import java.util.Map;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.AddFieldsOperation.AddFieldsOperationBuilder.ValueAppender;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 
 /**
  * Adds new fields to documents. {@code $addFields} outputs documents that contain all existing fields from the input
@@ -82,6 +83,7 @@ public static ValueAppender addField(String field) {
 	 * @param value the value to assign.
 	 * @return new instance of {@link AddFieldsOperation}.
 	 */
+	@Contract("_ -> new")
 	public AddFieldsOperation addField(Object field, Object value) {
 
 		LinkedHashMap<Object, Object> target = new LinkedHashMap<>(getValueMap());
@@ -95,6 +97,7 @@ public AddFieldsOperation addField(Object field, Object value) {
 	 *
 	 * @return new instance of {@link AddFieldsOperationBuilder}.
 	 */
+	@Contract("-> new")
 	public AddFieldsOperationBuilder and() {
 		return new AddFieldsOperationBuilder(getValueMap());
 	}
@@ -139,7 +142,7 @@ public ValueAppender addField(String field) {
 			return new ValueAppender() {
 
 				@Override
-				public AddFieldsOperationBuilder withValue(Object value) {
+				public AddFieldsOperationBuilder withValue(@Nullable Object value) {
 
 					valueMap.put(field, value);
 					return AddFieldsOperationBuilder.this;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressionTransformer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressionTransformer.java
index 00db38329f..e33c565d11 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressionTransformer.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressionTransformer.java
@@ -16,12 +16,12 @@
 package org.springframework.data.mongodb.core.aggregation;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.AggregationExpressionTransformer.AggregationExpressionTransformationContext;
 import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
 import org.springframework.data.mongodb.core.spel.ExpressionNode;
 import org.springframework.data.mongodb.core.spel.ExpressionTransformationContextSupport;
 import org.springframework.data.mongodb.core.spel.ExpressionTransformer;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
 /**
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationContext.java
index a49c7e46d5..5027328461 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationContext.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationContext.java
@@ -21,10 +21,10 @@
 
 import org.bson.Document;
 import org.bson.codecs.configuration.CodecRegistry;
+import org.jspecify.annotations.Nullable;
 import org.springframework.beans.BeanUtils;
 import org.springframework.data.mongodb.CodecRegistryProvider;
 import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ReflectionUtils;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationRenderer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationRenderer.java
index fd5f7ed979..6437ec981d 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationRenderer.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationRenderer.java
@@ -19,12 +19,12 @@
 import java.util.List;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.ExposedFields.DirectFieldReference;
 import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
 import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
 import org.springframework.data.mongodb.core.aggregation.Fields.AggregationField;
 import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation;
-import org.springframework.lang.Nullable;
 
 /**
  * Rendering support for {@link AggregationOperation} into a {@link List} of {@link org.bson.Document}.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOptions.java
index 327d40b8c7..278da408c6 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOptions.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOptions.java
@@ -19,11 +19,12 @@
 import java.util.Optional;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.ReadConcernAware;
 import org.springframework.data.mongodb.core.ReadPreferenceAware;
 import org.springframework.data.mongodb.core.query.Collation;
 import org.springframework.data.mongodb.util.BsonUtils;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 
 import com.mongodb.ReadConcern;
@@ -299,7 +300,7 @@ public boolean hasReadConcern() {
 	}
 
 	@Override
-	public ReadConcern getReadConcern() {
+	public @Nullable ReadConcern getReadConcern() {
 		return readConcern.orElse(null);
 	}
 
@@ -309,7 +310,7 @@ public boolean hasReadPreference() {
 	}
 
 	@Override
-	public ReadPreference getReadPreference() {
+	public @Nullable ReadPreference getReadPreference() {
 		return readPreference.orElse(null);
 	}
 
@@ -426,7 +427,7 @@ static Document createCursor(int cursorBatchSize) {
 	 */
 	public static class Builder {
 
-		private Boolean allowDiskUse;
+		private @Nullable Boolean allowDiskUse;
 		private boolean explain;
 		private @Nullable Document cursor;
 		private @Nullable Collation collation;
@@ -444,6 +445,7 @@ public static class Builder {
 		 * @param allowDiskUse use {@literal true} to allow disk use during the aggregation.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public Builder allowDiskUse(boolean allowDiskUse) {
 
 			this.allowDiskUse = allowDiskUse;
@@ -456,6 +458,7 @@ public Builder allowDiskUse(boolean allowDiskUse) {
 		 * @param explain use {@literal true} to enable explain feature.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public Builder explain(boolean explain) {
 
 			this.explain = explain;
@@ -468,6 +471,7 @@ public Builder explain(boolean explain) {
 		 * @param cursor must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public Builder cursor(Document cursor) {
 
 			this.cursor = cursor;
@@ -481,6 +485,7 @@ public Builder cursor(Document cursor) {
 		 * @return this.
 		 * @since 2.0
 		 */
+		@Contract("_ -> this")
 		public Builder cursorBatchSize(int batchSize) {
 
 			this.cursor = createCursor(batchSize);
@@ -494,6 +499,7 @@ public Builder cursorBatchSize(int batchSize) {
 		 * @return this.
 		 * @since 2.0
 		 */
+		@Contract("_ -> this")
 		public Builder collation(@Nullable Collation collation) {
 
 			this.collation = collation;
@@ -507,6 +513,7 @@ public Builder collation(@Nullable Collation collation) {
 		 * @return this.
 		 * @since 2.2
 		 */
+		@Contract("_ -> this")
 		public Builder comment(@Nullable String comment) {
 
 			this.comment = comment;
@@ -520,6 +527,7 @@ public Builder comment(@Nullable String comment) {
 		 * @return this.
 		 * @since 3.1
 		 */
+		@Contract("_ -> this")
 		public Builder hint(@Nullable Document hint) {
 
 			this.hint = hint;
@@ -533,6 +541,7 @@ public Builder hint(@Nullable Document hint) {
 		 * @return this.
 		 * @since 4.1
 		 */
+		@Contract("_ -> this")
 		public Builder hint(@Nullable String indexName) {
 
 			this.hint = indexName;
@@ -546,6 +555,7 @@ public Builder hint(@Nullable String indexName) {
 		 * @return this.
 		 * @since 4.1
 		 */
+		@Contract("_ -> this")
 		public Builder readConcern(@Nullable ReadConcern readConcern) {
 
 			this.readConcern = readConcern;
@@ -559,6 +569,7 @@ public Builder readConcern(@Nullable ReadConcern readConcern) {
 		 * @return this.
 		 * @since 4.1
 		 */
+		@Contract("_ -> this")
 		public Builder readPreference(@Nullable ReadPreference readPreference) {
 
 			this.readPreference = readPreference;
@@ -573,6 +584,7 @@ public Builder readPreference(@Nullable ReadPreference readPreference) {
 		 * @return this.
 		 * @since 3.0
 		 */
+		@Contract("_ -> this")
 		public Builder maxTime(@Nullable Duration maxTime) {
 
 			this.maxTime = maxTime;
@@ -587,6 +599,7 @@ public Builder maxTime(@Nullable Duration maxTime) {
 		 * @return this.
 		 * @since 3.0.2
 		 */
+		@Contract("-> this")
 		public Builder skipOutput() {
 
 			this.resultOptions = ResultOptions.SKIP;
@@ -600,6 +613,7 @@ public Builder skipOutput() {
 		 * @return this.
 		 * @since 3.2
 		 */
+		@Contract("-> this")
 		public Builder strictMapping() {
 
 			this.domainTypeMapping = DomainTypeMapping.STRICT;
@@ -613,6 +627,7 @@ public Builder strictMapping() {
 		 * @return this.
 		 * @since 3.2
 		 */
+		@Contract("-> this")
 		public Builder relaxedMapping() {
 
 			this.domainTypeMapping = DomainTypeMapping.RELAXED;
@@ -625,6 +640,7 @@ public Builder relaxedMapping() {
 		 * @return this.
 		 * @since 3.2
 		 */
+		@Contract("-> this")
 		public Builder noMapping() {
 
 			this.domainTypeMapping = DomainTypeMapping.NONE;
@@ -636,6 +652,7 @@ public Builder noMapping() {
 		 *
 		 * @return new instance of {@link AggregationOptions}.
 		 */
+		@Contract("-> new")
 		public AggregationOptions build() {
 
 			AggregationOptions options = new AggregationOptions(allowDiskUse, explain, cursor, collation, comment, hint);
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationPipeline.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationPipeline.java
index 68662ec0df..f06803997b 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationPipeline.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationPipeline.java
@@ -22,7 +22,10 @@
 import java.util.function.Predicate;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
 
 /**
  * The {@link AggregationPipeline} holds the collection of {@link AggregationOperation aggregation stages}.
@@ -63,6 +66,7 @@ public AggregationPipeline(List<AggregationOperation> aggregationOperations) {
 	 * @param aggregationOperation must not be {@literal null}.
 	 * @return this.
 	 */
+	@Contract("_ -> this")
 	public AggregationPipeline add(AggregationOperation aggregationOperation) {
 
 		Assert.notNull(aggregationOperation, "AggregationOperation must not be null");
@@ -80,6 +84,14 @@ public List<AggregationOperation> getOperations() {
 		return Collections.unmodifiableList(pipeline);
 	}
 
+	public @Nullable AggregationOperation firstOperation() {
+		return CollectionUtils.firstElement(pipeline);
+	}
+
+	public @Nullable AggregationOperation lastOperation() {
+		return CollectionUtils.lastElement(pipeline);
+	}
+
 	List<Document> toDocuments(AggregationOperationContext context) {
 
 		verify();
@@ -95,8 +107,8 @@ public boolean isOutOrMerge() {
 			return false;
 		}
 
-		AggregationOperation operation = pipeline.get(pipeline.size() - 1);
-		return isOut(operation) || isMerge(operation);
+		AggregationOperation operation = lastOperation();
+		return operation != null && (isOut(operation) || isMerge(operation));
 	}
 
 	void verify() {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationResults.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationResults.java
index 438eb9e49f..7b27739229 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationResults.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationResults.java
@@ -20,7 +20,7 @@
 import java.util.List;
 
 import org.bson.Document;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.Assert;
 
 /**
@@ -71,8 +71,7 @@ public List<T> getMappedResults() {
 	 * @return the single already mapped result object or raise an error if more than one found.
 	 * @throws IllegalArgumentException in case more than one result is available.
 	 */
-	@Nullable
-	public T getUniqueMappedResult() {
+	public @Nullable T getUniqueMappedResult() {
 		Assert.isTrue(mappedResults.size() < 2, "Expected unique result or null, but got more than one");
 		return mappedResults.size() == 1 ? mappedResults.get(0) : null;
 	}
@@ -101,10 +100,10 @@ public Document getRawResults() {
 		return rawResults;
 	}
 
-	@Nullable
-	private String parseServerUsed() {
+	private @Nullable String parseServerUsed() {
 
 		Object object = rawResults.get("serverUsed");
 		return object instanceof String stringValue ? stringValue : null;
 	}
+
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationSpELExpression.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationSpELExpression.java
index 1626d672bc..c5b53ef0c6 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationSpELExpression.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationSpELExpression.java
@@ -66,6 +66,8 @@ public static AggregationSpELExpression expressionOf(String expressionString, Ob
 
 	@Override
 	public Document toDocument(AggregationOperationContext context) {
-		return (Document) TRANSFORMER.transform(rawExpression, context, parameters);
+
+		Document doc = (Document) TRANSFORMER.transform(rawExpression, context, parameters);
+		return doc != null ? doc : new Document();
 	}
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationUpdate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationUpdate.java
index 15d700309e..9e8564c03e 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationUpdate.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationUpdate.java
@@ -25,11 +25,11 @@
 import java.util.stream.Collectors;
 
 import org.bson.Document;
-
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mongodb.core.query.SerializationUtils;
 import org.springframework.data.mongodb.core.query.UpdateDefinition;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 
 /**
@@ -129,6 +129,7 @@ public static AggregationUpdate from(List<AggregationOperation> pipeline) {
 	 * @return this.
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/set/">$set Aggregation Reference</a>
 	 */
+	@Contract("_ -> this")
 	public AggregationUpdate set(SetOperation setOperation) {
 
 		Assert.notNull(setOperation, "SetOperation must not be null");
@@ -148,6 +149,7 @@ public AggregationUpdate set(SetOperation setOperation) {
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/unset/">$unset Aggregation
 	 *      Reference</a>
 	 */
+	@Contract("_ -> this")
 	public AggregationUpdate unset(UnsetOperation unsetOperation) {
 
 		Assert.notNull(unsetOperation, "UnsetOperation must not be null");
@@ -166,6 +168,7 @@ public AggregationUpdate unset(UnsetOperation unsetOperation) {
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/replaceWith/">$replaceWith Aggregation
 	 *      Reference</a>
 	 */
+	@Contract("_ -> this")
 	public AggregationUpdate replaceWith(ReplaceWithOperation replaceWithOperation) {
 
 		Assert.notNull(replaceWithOperation, "ReplaceWithOperation must not be null");
@@ -179,6 +182,7 @@ public AggregationUpdate replaceWith(ReplaceWithOperation replaceWithOperation)
 	 * @param value must not be {@literal null}.
 	 * @return this.
 	 */
+	@Contract("_ -> this")
 	public AggregationUpdate replaceWith(Object value) {
 
 		Assert.notNull(value, "Value must not be null");
@@ -193,6 +197,7 @@ public AggregationUpdate replaceWith(Object value) {
 	 * @return new instance of {@link SetValueAppender}.
 	 * @see #set(SetOperation)
 	 */
+	@Contract("_ -> new")
 	public SetValueAppender set(String key) {
 
 		Assert.notNull(key, "Key must not be null");
@@ -219,6 +224,7 @@ public AggregationUpdate toValueOf(Object value) {
 	 * @param keys the fields to remove.
 	 * @return this.
 	 */
+	@Contract("_ -> this")
 	public AggregationUpdate unset(String... keys) {
 
 		Assert.notNull(keys, "Keys must not be null");
@@ -234,6 +240,7 @@ public AggregationUpdate unset(String... keys) {
 	 *
 	 * @return never {@literal null}.
 	 */
+	@Contract("-> this")
 	public AggregationUpdate isolated() {
 
 		isolated = true;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationVariable.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationVariable.java
index ed79202345..522dd5eae5 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationVariable.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationVariable.java
@@ -15,7 +15,7 @@
  */
 package org.springframework.data.mongodb.core.aggregation;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArithmeticOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArithmeticOperators.java
index e2c31c6346..c7787b382c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArithmeticOperators.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArithmeticOperators.java
@@ -20,6 +20,7 @@
 import java.util.Locale;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Avg;
 import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.CovariancePop;
 import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.CovarianceSamp;
@@ -32,7 +33,7 @@
 import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Sum;
 import org.springframework.data.mongodb.core.aggregation.SetWindowFieldsOperation.WindowUnit;
 import org.springframework.data.mongodb.core.aggregation.SetWindowFieldsOperation.WindowUnits;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
@@ -84,8 +85,8 @@ public static Rand rand() {
 	 */
 	public static class ArithmeticOperatorFactory {
 
-		private final String fieldReference;
-		private final AggregationExpression expression;
+		private final @Nullable String fieldReference;
+		private final @Nullable AggregationExpression expression;
 
 		/**
 		 * Creates new {@link ArithmeticOperatorFactory} for given {@literal fieldReference}.
@@ -116,6 +117,7 @@ public ArithmeticOperatorFactory(AggregationExpression expression) {
 		 *
 		 * @return new instance of {@link Abs}.
 		 */
+		@SuppressWarnings("NullAway")
 		public Abs abs() {
 			return usesFieldRef() ? Abs.absoluteValueOf(fieldReference) : Abs.absoluteValueOf(expression);
 		}
@@ -158,6 +160,7 @@ public Add add(Number value) {
 			return createAdd().add(value);
 		}
 
+		@SuppressWarnings("NullAway")
 		private Add createAdd() {
 			return usesFieldRef() ? Add.valueOf(fieldReference) : Add.valueOf(expression);
 		}
@@ -168,6 +171,7 @@ private Add createAdd() {
 		 *
 		 * @return new instance of {@link Ceil}.
 		 */
+		@SuppressWarnings("NullAway")
 		public Ceil ceil() {
 			return usesFieldRef() ? Ceil.ceilValueOf(fieldReference) : Ceil.ceilValueOf(expression);
 		}
@@ -205,6 +209,7 @@ public Derivative derivative(WindowUnit unit) {
 		 * @return new instance of {@link Derivative}.
 		 * @since 3.3
 		 */
+		@SuppressWarnings("NullAway")
 		public Derivative derivative(@Nullable String unit) {
 
 			Derivative derivative = usesFieldRef() ? Derivative.derivativeOf(fieldReference)
@@ -250,6 +255,7 @@ public Divide divideBy(Number value) {
 			return createDivide().divideBy(value);
 		}
 
+		@SuppressWarnings("NullAway")
 		private Divide createDivide() {
 			return usesFieldRef() ? Divide.valueOf(fieldReference) : Divide.valueOf(expression);
 		}
@@ -259,6 +265,7 @@ private Divide createDivide() {
 		 *
 		 * @return new instance of {@link Exp}.
 		 */
+		@SuppressWarnings("NullAway")
 		public Exp exp() {
 			return usesFieldRef() ? Exp.expValueOf(fieldReference) : Exp.expValueOf(expression);
 		}
@@ -269,6 +276,7 @@ public Exp exp() {
 		 *
 		 * @return new instance of {@link Floor}.
 		 */
+		@SuppressWarnings("NullAway")
 		public Floor floor() {
 			return usesFieldRef() ? Floor.floorValueOf(fieldReference) : Floor.floorValueOf(expression);
 		}
@@ -279,6 +287,7 @@ public Floor floor() {
 		 * @return new instance of {@link Integral}.
 		 * @since 3.3
 		 */
+		@SuppressWarnings("NullAway")
 		public Integral integral() {
 			return usesFieldRef() ? Integral.integralOf(fieldReference) : Integral.integralOf(expression);
 		}
@@ -318,6 +327,7 @@ public Integral integral(String unit) {
 		 *
 		 * @return new instance of {@link Ln}.
 		 */
+		@SuppressWarnings("NullAway")
 		public Ln ln() {
 			return usesFieldRef() ? Ln.lnValueOf(fieldReference) : Ln.lnValueOf(expression);
 		}
@@ -345,7 +355,7 @@ public Log log(String fieldReference) {
 		public Log log(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
-			return createLog().log(fieldReference);
+			return createLog().log(expression);
 		}
 
 		/**
@@ -361,6 +371,7 @@ public Log log(Number base) {
 			return createLog().log(base);
 		}
 
+		@SuppressWarnings("NullAway")
 		private Log createLog() {
 			return usesFieldRef() ? Log.valueOf(fieldReference) : Log.valueOf(expression);
 		}
@@ -370,6 +381,7 @@ private Log createLog() {
 		 *
 		 * @return new instance of {@link Log10}.
 		 */
+		@SuppressWarnings("NullAway")
 		public Log10 log10() {
 			return usesFieldRef() ? Log10.log10ValueOf(fieldReference) : Log10.log10ValueOf(expression);
 		}
@@ -413,6 +425,7 @@ public Mod mod(Number value) {
 			return createMod().mod(value);
 		}
 
+		@SuppressWarnings("NullAway")
 		private Mod createMod() {
 			return usesFieldRef() ? Mod.valueOf(fieldReference) : Mod.valueOf(expression);
 		}
@@ -453,6 +466,7 @@ public Multiply multiplyBy(Number value) {
 			return createMultiply().multiplyBy(value);
 		}
 
+		@SuppressWarnings("NullAway")
 		private Multiply createMultiply() {
 			return usesFieldRef() ? Multiply.valueOf(fieldReference) : Multiply.valueOf(expression);
 		}
@@ -493,6 +507,7 @@ public Pow pow(Number value) {
 			return createPow().pow(value);
 		}
 
+		@SuppressWarnings("NullAway")
 		private Pow createPow() {
 			return usesFieldRef() ? Pow.valueOf(fieldReference) : Pow.valueOf(expression);
 		}
@@ -502,6 +517,7 @@ private Pow createPow() {
 		 *
 		 * @return new instance of {@link Sqrt}.
 		 */
+		@SuppressWarnings("NullAway")
 		public Sqrt sqrt() {
 			return usesFieldRef() ? Sqrt.sqrtOf(fieldReference) : Sqrt.sqrtOf(expression);
 		}
@@ -542,6 +558,7 @@ public Subtract subtract(Number value) {
 			return createSubtract().subtract(value);
 		}
 
+		@SuppressWarnings("NullAway")
 		private Subtract createSubtract() {
 			return usesFieldRef() ? Subtract.valueOf(fieldReference) : Subtract.valueOf(expression);
 		}
@@ -551,6 +568,7 @@ private Subtract createSubtract() {
 		 *
 		 * @return new instance of {@link Trunc}.
 		 */
+		@SuppressWarnings("NullAway")
 		public Trunc trunc() {
 			return usesFieldRef() ? Trunc.truncValueOf(fieldReference) : Trunc.truncValueOf(expression);
 		}
@@ -560,6 +578,7 @@ public Trunc trunc() {
 		 *
 		 * @return new instance of {@link Sum}.
 		 */
+		@SuppressWarnings("NullAway")
 		public Sum sum() {
 			return usesFieldRef() ? AccumulatorOperators.Sum.sumOf(fieldReference)
 					: AccumulatorOperators.Sum.sumOf(expression);
@@ -570,6 +589,7 @@ public Sum sum() {
 		 *
 		 * @return new instance of {@link Avg}.
 		 */
+		@SuppressWarnings("NullAway")
 		public Avg avg() {
 			return usesFieldRef() ? AccumulatorOperators.Avg.avgOf(fieldReference)
 					: AccumulatorOperators.Avg.avgOf(expression);
@@ -580,6 +600,7 @@ public Avg avg() {
 		 *
 		 * @return new instance of {@link Max}.
 		 */
+		@SuppressWarnings("NullAway")
 		public Max max() {
 			return usesFieldRef() ? AccumulatorOperators.Max.maxOf(fieldReference)
 					: AccumulatorOperators.Max.maxOf(expression);
@@ -590,6 +611,7 @@ public Max max() {
 		 *
 		 * @return new instance of {@link Min}.
 		 */
+		@SuppressWarnings("NullAway")
 		public Min min() {
 			return usesFieldRef() ? AccumulatorOperators.Min.minOf(fieldReference)
 					: AccumulatorOperators.Min.minOf(expression);
@@ -600,6 +622,7 @@ public Min min() {
 		 *
 		 * @return new instance of {@link StdDevPop}.
 		 */
+		@SuppressWarnings("NullAway")
 		public StdDevPop stdDevPop() {
 			return usesFieldRef() ? AccumulatorOperators.StdDevPop.stdDevPopOf(fieldReference)
 					: AccumulatorOperators.StdDevPop.stdDevPopOf(expression);
@@ -610,6 +633,7 @@ public StdDevPop stdDevPop() {
 		 *
 		 * @return new instance of {@link StdDevSamp}.
 		 */
+		@SuppressWarnings("NullAway")
 		public StdDevSamp stdDevSamp() {
 			return usesFieldRef() ? AccumulatorOperators.StdDevSamp.stdDevSampOf(fieldReference)
 					: AccumulatorOperators.StdDevSamp.stdDevSampOf(expression);
@@ -639,6 +663,7 @@ public CovariancePop covariancePop(AggregationExpression expression) {
 			return covariancePop().and(expression);
 		}
 
+		@SuppressWarnings("NullAway")
 		private CovariancePop covariancePop() {
 			return usesFieldRef() ? CovariancePop.covariancePopOf(fieldReference) : CovariancePop.covariancePopOf(expression);
 		}
@@ -667,6 +692,7 @@ public CovarianceSamp covarianceSamp(AggregationExpression expression) {
 			return covarianceSamp().and(expression);
 		}
 
+		@SuppressWarnings("NullAway")
 		private CovarianceSamp covarianceSamp() {
 			return usesFieldRef() ? CovarianceSamp.covarianceSampOf(fieldReference)
 					: CovarianceSamp.covarianceSampOf(expression);
@@ -679,6 +705,7 @@ private CovarianceSamp covarianceSamp() {
 		 * @return new instance of {@link Round}.
 		 * @since 3.0
 		 */
+		@SuppressWarnings("NullAway")
 		public Round round() {
 			return usesFieldRef() ? Round.roundValueOf(fieldReference) : Round.roundValueOf(expression);
 		}
@@ -712,6 +739,7 @@ public Sin sin() {
 		 * @return new instance of {@link Sin}.
 		 * @since 3.3
 		 */
+		@SuppressWarnings("NullAway")
 		public Sin sin(AngularUnit unit) {
 			return usesFieldRef() ? Sin.sinOf(fieldReference, unit) : Sin.sinOf(expression, unit);
 		}
@@ -734,6 +762,7 @@ public Sinh sinh() {
 		 * @return new instance of {@link Sinh}.
 		 * @since 3.3
 		 */
+		@SuppressWarnings("NullAway")
 		public Sinh sinh(AngularUnit unit) {
 			return usesFieldRef() ? Sinh.sinhOf(fieldReference, unit) : Sinh.sinhOf(expression, unit);
 		}
@@ -744,6 +773,7 @@ public Sinh sinh(AngularUnit unit) {
 		 * @return new instance of {@link ASin}.
 		 * @since 3.3
 		 */
+		@SuppressWarnings("NullAway")
 		public ASin asin() {
 			return usesFieldRef() ? ASin.asinOf(fieldReference) : ASin.asinOf(expression);
 		}
@@ -754,6 +784,7 @@ public ASin asin() {
 		 * @return new instance of {@link ASinh}.
 		 * @since 3.3
 		 */
+		@SuppressWarnings("NullAway")
 		public ASinh asinh() {
 			return usesFieldRef() ? ASinh.asinhOf(fieldReference) : ASinh.asinhOf(expression);
 		}
@@ -777,6 +808,7 @@ public Cos cos() {
 		 * @return new instance of {@link Cos}.
 		 * @since 3.3
 		 */
+		@SuppressWarnings("NullAway")
 		public Cos cos(AngularUnit unit) {
 			return usesFieldRef() ? Cos.cosOf(fieldReference, unit) : Cos.cosOf(expression, unit);
 		}
@@ -799,6 +831,7 @@ public Cosh cosh() {
 		 * @return new instance of {@link Cosh}.
 		 * @since 3.3
 		 */
+		@SuppressWarnings("NullAway")
 		public Cosh cosh(AngularUnit unit) {
 			return usesFieldRef() ? Cosh.coshOf(fieldReference, unit) : Cosh.coshOf(expression, unit);
 		}
@@ -809,6 +842,7 @@ public Cosh cosh(AngularUnit unit) {
 		 * @return new instance of {@link ACos}.
 		 * @since 3.4
 		 */
+		@SuppressWarnings("NullAway")
 		public ACos acos() {
 			return usesFieldRef() ? ACos.acosOf(fieldReference) : ACos.acosOf(expression);
 		}
@@ -819,6 +853,7 @@ public ACos acos() {
 		 * @return new instance of {@link ACosh}.
 		 * @since 3.4
 		 */
+		@SuppressWarnings("NullAway")
 		public ACosh acosh() {
 			return usesFieldRef() ? ACosh.acoshOf(fieldReference) : ACosh.acoshOf(expression);
 		}
@@ -840,6 +875,7 @@ public Tan tan() {
 		 * @return new instance of {@link ATan}.
 		 * @since 3.3
 		 */
+		@SuppressWarnings("NullAway")
 		public ATan atan() {
 			return usesFieldRef() ? ATan.atanOf(fieldReference) : ATan.atanOf(expression);
 		}
@@ -852,6 +888,7 @@ public ATan atan() {
 		 * @return new instance of {@link ATan2}.
 		 * @since 3.3
 		 */
+		@SuppressWarnings("NullAway")
 		public ATan2 atan2(Number value) {
 
 			Assert.notNull(value, "Value must not be null");
@@ -886,8 +923,8 @@ public ATan2 atan2(AggregationExpression expression) {
 			return createATan2().atan2of(expression);
 		}
 
+		@SuppressWarnings("NullAway")
 		private ATan2 createATan2() {
-
 			return usesFieldRef() ? ATan2.valueOf(fieldReference) : ATan2.valueOf(expression);
 		}
 
@@ -897,6 +934,7 @@ private ATan2 createATan2() {
 		 * @return new instance of {@link ATanh}.
 		 * @since 3.3
 		 */
+		@SuppressWarnings("NullAway")
 		public ATanh atanh() {
 			return usesFieldRef() ? ATanh.atanhOf(fieldReference) : ATanh.atanhOf(expression);
 		}
@@ -909,6 +947,7 @@ public ATanh atanh() {
 		 * @return new instance of {@link Tan}.
 		 * @since 3.3
 		 */
+		@SuppressWarnings("NullAway")
 		public Tan tan(AngularUnit unit) {
 			return usesFieldRef() ? Tan.tanOf(fieldReference, unit) : Tan.tanOf(expression, unit);
 		}
@@ -931,18 +970,19 @@ public Tanh tanh() {
 		 * @return new instance of {@link Tanh}.
 		 * @since 3.3
 		 */
+		@SuppressWarnings("NullAway")
 		public Tanh tanh(AngularUnit unit) {
 			return usesFieldRef() ? Tanh.tanhOf(fieldReference, unit) : Tanh.tanhOf(expression, unit);
 		}
 
 		/**
-		 * Creates new {@link AggregationExpression} that calculates the requested percentile(s) of the
-		 * numeric value.
+		 * Creates new {@link AggregationExpression} that calculates the requested percentile(s) of the numeric value.
 		 *
 		 * @return new instance of {@link Percentile}.
 		 * @param percentages must not be {@literal null}.
 		 * @since 4.2
 		 */
+		@SuppressWarnings("NullAway")
 		public Percentile percentile(Double... percentages) {
 			Percentile percentile = usesFieldRef() ? AccumulatorOperators.Percentile.percentileOf(fieldReference)
 					: AccumulatorOperators.Percentile.percentileOf(expression);
@@ -950,12 +990,12 @@ public Percentile percentile(Double... percentages) {
 		}
 
 		/**
-		 * Creates new {@link AggregationExpression} that calculates the requested percentile(s) of the
-		 * numeric value.
+		 * Creates new {@link AggregationExpression} that calculates the requested percentile(s) of the numeric value.
 		 *
 		 * @return new instance of {@link Median}.
 		 * @since 4.2
 		 */
+		@SuppressWarnings("NullAway")
 		public Median median() {
 			return usesFieldRef() ? AccumulatorOperators.Median.medianOf(fieldReference)
 					: AccumulatorOperators.Median.medianOf(expression);
@@ -1077,6 +1117,7 @@ public static Add valueOf(Number value) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link Add}.
 		 */
+		@Contract("_ -> new")
 		public Add add(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -1089,6 +1130,7 @@ public Add add(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Add}.
 		 */
+		@Contract("_ -> new")
 		public Add add(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -1101,6 +1143,7 @@ public Add add(AggregationExpression expression) {
 		 * @param value must not be {@literal null}.
 		 * @return new instance of {@link Add}.
 		 */
+		@Contract("_ -> new")
 		public Add add(Number value) {
 			return new Add(append(value));
 		}
@@ -1217,6 +1260,7 @@ public static Divide valueOf(Number value) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link Divide}.
 		 */
+		@Contract("_ -> new")
 		public Divide divideBy(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -1229,6 +1273,7 @@ public Divide divideBy(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Divide}.
 		 */
+		@Contract("_ -> new")
 		public Divide divideBy(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -1241,6 +1286,7 @@ public Divide divideBy(AggregationExpression expression) {
 		 * @param value must not be {@literal null}.
 		 * @return new instance of {@link Divide}.
 		 */
+		@Contract("_ -> new")
 		public Divide divideBy(Number value) {
 			return new Divide(append(value));
 		}
@@ -1463,6 +1509,7 @@ public static Log valueOf(Number value) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link Log}.
 		 */
+		@Contract("_ -> new")
 		public Log log(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -1475,6 +1522,7 @@ public Log log(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Log}.
 		 */
+		@Contract("_ -> new")
 		public Log log(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -1487,6 +1535,7 @@ public Log log(AggregationExpression expression) {
 		 * @param base must not be {@literal null}.
 		 * @return new instance of {@link Log}.
 		 */
+		@Contract("_ -> new")
 		public Log log(Number base) {
 			return new Log(append(base));
 		}
@@ -1603,6 +1652,7 @@ public static Mod valueOf(Number value) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link Mod}.
 		 */
+		@Contract("_ -> new")
 		public Mod mod(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -1615,6 +1665,7 @@ public Mod mod(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Mod}.
 		 */
+		@Contract("_ -> new")
 		public Mod mod(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -1627,6 +1678,7 @@ public Mod mod(AggregationExpression expression) {
 		 * @param base must not be {@literal null}.
 		 * @return new instance of {@link Mod}.
 		 */
+		@Contract("_ -> new")
 		public Mod mod(Number base) {
 			return new Mod(append(base));
 		}
@@ -1690,6 +1742,7 @@ public static Multiply valueOf(Number value) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link Multiply}.
 		 */
+		@Contract("_ -> new")
 		public Multiply multiplyBy(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -1702,6 +1755,7 @@ public Multiply multiplyBy(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Multiply}.
 		 */
+		@Contract("_ -> new")
 		public Multiply multiplyBy(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -1714,6 +1768,7 @@ public Multiply multiplyBy(AggregationExpression expression) {
 		 * @param value must not be {@literal null}.
 		 * @return new instance of {@link Multiply}.
 		 */
+		@Contract("_ -> new")
 		public Multiply multiplyBy(Number value) {
 			return new Multiply(append(value));
 		}
@@ -1777,6 +1832,7 @@ public static Pow valueOf(Number value) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link Pow}.
 		 */
+		@Contract("_ -> new")
 		public Pow pow(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -1789,6 +1845,7 @@ public Pow pow(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Pow}.
 		 */
+		@Contract("_ -> new")
 		public Pow pow(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -1801,6 +1858,7 @@ public Pow pow(AggregationExpression expression) {
 		 * @param value must not be {@literal null}.
 		 * @return new instance of {@link Pow}.
 		 */
+		@Contract("_ -> new")
 		public Pow pow(Number value) {
 			return new Pow(append(value));
 		}
@@ -1917,6 +1975,7 @@ public static Subtract valueOf(Number value) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link Pow}.
 		 */
+		@Contract("_ -> new")
 		public Subtract subtract(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -1929,6 +1988,7 @@ public Subtract subtract(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Pow}.
 		 */
+		@Contract("_ -> new")
 		public Subtract subtract(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -1941,6 +2001,7 @@ public Subtract subtract(AggregationExpression expression) {
 		 * @param value must not be {@literal null}.
 		 * @return new instance of {@link Pow}.
 		 */
+		@Contract("_ -> new")
 		public Subtract subtract(Number value) {
 			return new Subtract(append(value));
 		}
@@ -2060,6 +2121,7 @@ public static Round round(Number value) {
 		 * @param place value between -20 and 100, exclusive.
 		 * @return new instance of {@link Round}.
 		 */
+		@Contract("_ -> new")
 		public Round place(int place) {
 			return new Round(append(place));
 		}
@@ -2070,6 +2132,7 @@ public Round place(int place) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Round}.
 		 */
+		@Contract("_ -> new")
 		public Round placeOf(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -2083,6 +2146,7 @@ public Round placeOf(AggregationExpression expression) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link Round}.
 		 */
+		@Contract("_ -> new")
 		public Round placeOf(String fieldReference) {
 
 			Assert.notNull(fieldReference, "fieldReference must not be null");
@@ -2133,6 +2197,7 @@ public static Derivative derivativeOfValue(Number value) {
 			return new Derivative(Collections.singletonMap("input", value));
 		}
 
+		@Contract("_ -> new")
 		public Derivative unit(String unit) {
 			return new Derivative(append("unit", unit));
 		}
@@ -2183,6 +2248,7 @@ public static Integral integralOf(AggregationExpression expression) {
 		 * @param unit the unit of measure.
 		 * @return new instance of {@link Integral}.
 		 */
+		@Contract("_ -> new")
 		public Integral unit(String unit) {
 			return new Integral(append("unit", unit));
 		}
@@ -2217,8 +2283,7 @@ private Sin(Object value) {
 
 		/**
 		 * Creates a new {@link AggregationExpression} that calculates the sine of a value that is measured in
-		 * {@link AngularUnit#RADIANS radians}.
-		 * <br />
+		 * {@link AngularUnit#RADIANS radians}. <br />
 		 * Use {@code sinhOf("angle", DEGREES)} as shortcut for
 		 *
 		 * <pre>
@@ -2330,8 +2395,7 @@ public static Sinh sinhOf(String fieldReference) {
 
 		/**
 		 * Creates a new {@link AggregationExpression} that calculates the hyperbolic sine of a value that is measured in
-		 * the given {@link AngularUnit unit}.
-		 * <br />
+		 * the given {@link AngularUnit unit}. <br />
 		 * Use {@code sinhOf("angle", DEGREES)} as shortcut for
 		 *
 		 * <pre>
@@ -2350,8 +2414,7 @@ public static Sinh sinhOf(String fieldReference, AngularUnit unit) {
 
 		/**
 		 * Creates a new {@link AggregationExpression} that calculates the hyperbolic sine of a value that is measured in
-		 * {@link AngularUnit#RADIANS}.
-		 * <br />
+		 * {@link AngularUnit#RADIANS}. <br />
 		 * Use {@code sinhOf("angle", DEGREES)} as shortcut for eg.
 		 * {@code sinhOf(ConvertOperators.valueOf("angle").degreesToRadians())}.
 		 *
@@ -2434,8 +2497,7 @@ public static ASin asinOf(String fieldReference) {
 		}
 
 		/**
-		 * Creates a new {@link AggregationExpression} that calculates the inverse sine of a value.
-		 * <br />
+		 * Creates a new {@link AggregationExpression} that calculates the inverse sine of a value. <br />
 		 *
 		 * @param expression the {@link AggregationExpression expression} that resolves to a numeric value.
 		 * @return new instance of {@link ASin}.
@@ -2484,8 +2546,7 @@ public static ASinh asinhOf(String fieldReference) {
 		}
 
 		/**
-		 * Creates a new {@link AggregationExpression} that calculates the inverse hyperbolic sine of a value.
-		 * <br />
+		 * Creates a new {@link AggregationExpression} that calculates the inverse hyperbolic sine of a value. <br />
 		 *
 		 * @param expression the {@link AggregationExpression expression} that resolves to a numeric value.
 		 * @return new instance of {@link ASinh}.
@@ -2525,8 +2586,7 @@ private Cos(Object value) {
 
 		/**
 		 * Creates a new {@link AggregationExpression} that calculates the cosine of a value that is measured in
-		 * {@link AngularUnit#RADIANS radians}.
-		 * <br />
+		 * {@link AngularUnit#RADIANS radians}. <br />
 		 * Use {@code cosOf("angle", DEGREES)} as shortcut for
 		 *
 		 * <pre>
@@ -2636,8 +2696,7 @@ public static Cosh coshOf(String fieldReference) {
 
 		/**
 		 * Creates a new {@link AggregationExpression} that calculates the hyperbolic cosine of a value that is measured in
-		 * the given {@link AngularUnit unit}.
-		 * <br />
+		 * the given {@link AngularUnit unit}. <br />
 		 * Use {@code coshOf("angle", DEGREES)} as shortcut for
 		 *
 		 * <pre>
@@ -2654,8 +2713,7 @@ public static Cosh coshOf(String fieldReference, AngularUnit unit) {
 
 		/**
 		 * Creates a new {@link AggregationExpression} that calculates the hyperbolic cosine of a value that is measured in
-		 * {@link AngularUnit#RADIANS}.
-		 * <br />
+		 * {@link AngularUnit#RADIANS}. <br />
 		 * Use {@code sinhOf("angle", DEGREES)} as shortcut for eg.
 		 * {@code sinhOf(ConvertOperators.valueOf("angle").degreesToRadians())}.
 		 *
@@ -2738,8 +2796,7 @@ public static ACos acosOf(String fieldReference) {
 		}
 
 		/**
-		 * Creates a new {@link AggregationExpression} that calculates the inverse cosine of a value.
-		 * <br />
+		 * Creates a new {@link AggregationExpression} that calculates the inverse cosine of a value. <br />
 		 *
 		 * @param expression the {@link AggregationExpression expression} that resolves to a numeric value.
 		 * @return new instance of {@link ACos}.
@@ -2788,8 +2845,7 @@ public static ACosh acoshOf(String fieldReference) {
 		}
 
 		/**
-		 * Creates a new {@link AggregationExpression} that calculates the inverse hyperbolic cosine of a value.
-		 * <br />
+		 * Creates a new {@link AggregationExpression} that calculates the inverse hyperbolic cosine of a value. <br />
 		 *
 		 * @param expression the {@link AggregationExpression expression} that resolves to a numeric value.
 		 * @return new instance of {@link ACosh}.
@@ -2829,8 +2885,7 @@ private Tan(Object value) {
 
 		/**
 		 * Creates a new {@link AggregationExpression} that calculates the tangent of a value that is measured in
-		 * {@link AngularUnit#RADIANS radians}.
-		 * <br />
+		 * {@link AngularUnit#RADIANS radians}. <br />
 		 * Use {@code tanOf("angle", DEGREES)} as shortcut for
 		 *
 		 * <pre>
@@ -3008,8 +3063,8 @@ public static ATan2 valueOf(AggregationExpression expression) {
 		 * Creates a new {@link AggregationExpression} that calculates the inverse tangent of of y / x, where y and x are
 		 * the first and second values passed to the expression respectively.
 		 *
-		 * @param fieldReference anything ({@link Field field}, {@link AggregationExpression expression}, ...) that resolves to a
-		 *          numeric value.
+		 * @param fieldReference anything ({@link Field field}, {@link AggregationExpression expression}, ...) that resolves
+		 *          to a numeric value.
 		 * @return new instance of {@link ATan2}.
 		 */
 		public ATan2 atan2of(String fieldReference) {
@@ -3022,8 +3077,8 @@ public ATan2 atan2of(String fieldReference) {
 		 * Creates a new {@link AggregationExpression} that calculates the hyperbolic tangent of a value that is measured in
 		 * {@link AngularUnit#RADIANS}.
 		 *
-		 * @param expression anything ({@link Field field}, {@link AggregationExpression expression}, ...) that resolves to a
-		 *          numeric value.
+		 * @param expression anything ({@link Field field}, {@link AggregationExpression expression}, ...) that resolves to
+		 *          a numeric value.
 		 * @return new instance of {@link ATan2}.
 		 */
 		public ATan2 atan2of(AggregationExpression expression) {
@@ -3075,8 +3130,7 @@ public static Tanh tanhOf(String fieldReference) {
 
 		/**
 		 * Creates a new {@link AggregationExpression} that calculates the hyperbolic tangent of a value that is measured in
-		 * the given {@link AngularUnit unit}.
-		 * <br />
+		 * the given {@link AngularUnit unit}. <br />
 		 * Use {@code tanhOf("angle", DEGREES)} as shortcut for
 		 *
 		 * <pre>
@@ -3093,8 +3147,7 @@ public static Tanh tanhOf(String fieldReference, AngularUnit unit) {
 
 		/**
 		 * Creates a new {@link AggregationExpression} that calculates the hyperbolic tangent of a value that is measured in
-		 * {@link AngularUnit#RADIANS}.
-		 * <br />
+		 * {@link AngularUnit#RADIANS}. <br />
 		 * Use {@code sinhOf("angle", DEGREES)} as shortcut for eg.
 		 * {@code sinhOf(ConvertOperators.valueOf("angle").degreesToRadians())}.
 		 *
@@ -3165,11 +3218,9 @@ private ATanh(Object value) {
 		}
 
 		/**
-		 * Creates a new {@link AggregationExpression} that calculates the inverse
-		 * hyperbolic tangent of a value.
+		 * Creates a new {@link AggregationExpression} that calculates the inverse hyperbolic tangent of a value.
 		 *
-		 * @param fieldReference the name of the {@link Field field} that resolves to a
-		 *                       numeric value.
+		 * @param fieldReference the name of the {@link Field field} that resolves to a numeric value.
 		 * @return new instance of {@link ATanh}.
 		 */
 		public static ATanh atanhOf(String fieldReference) {
@@ -3177,8 +3228,7 @@ public static ATanh atanhOf(String fieldReference) {
 		}
 
 		/**
-		 * Creates a new {@link AggregationExpression} that calculates the inverse hyperbolic tangent of a value.
-		 * <br />
+		 * Creates a new {@link AggregationExpression} that calculates the inverse hyperbolic tangent of a value. <br />
 		 *
 		 * @param expression the {@link AggregationExpression expression} that resolves to a numeric value.
 		 * @return new instance of {@link ATanh}.
@@ -3188,11 +3238,10 @@ public static ATanh atanhOf(AggregationExpression expression) {
 		}
 
 		/**
-		 * Creates a new {@link AggregationExpression} that calculates the inverse
-		 * hyperbolic tangent of a value.
+		 * Creates a new {@link AggregationExpression} that calculates the inverse hyperbolic tangent of a value.
 		 *
-		 * @param value anything ({@link Field field}, {@link AggregationExpression
-		 *              expression}, ...) that resolves to a numeric value.
+		 * @param value anything ({@link Field field}, {@link AggregationExpression expression}, ...) that resolves to a
+		 *          numeric value.
 		 * @return new instance of {@link ATanh}.
 		 */
 		public static ATanh atanhOf(Object value) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java
index 41688bfc62..85952d8f39 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java
@@ -22,12 +22,14 @@
 import java.util.List;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.domain.Range;
 import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Sort.Direction;
 import org.springframework.data.mongodb.core.aggregation.ArrayOperators.Filter.AsBuilder;
 import org.springframework.data.mongodb.core.aggregation.ArrayOperators.Reduce.PropertyExpression;
 import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 
 /**
@@ -158,6 +160,7 @@ public ArrayElemAt elementAt(String fieldReference) {
 			return createArrayElemAt().elementAt(fieldReference);
 		}
 
+		@SuppressWarnings("NullAway")
 		private ArrayElemAt createArrayElemAt() {
 
 			if (usesFieldRef()) {
@@ -193,6 +196,7 @@ public ConcatArrays concat(AggregationExpression expression) {
 			return createConcatArrays().concat(expression);
 		}
 
+		@SuppressWarnings("NullAway")
 		private ConcatArrays createConcatArrays() {
 
 			if (usesFieldRef()) {
@@ -208,6 +212,7 @@ private ConcatArrays createConcatArrays() {
 		 *
 		 * @return new instance of {@link AsBuilder} to create a {@link Filter}.
 		 */
+		@SuppressWarnings("NullAway")
 		public AsBuilder filter() {
 
 			if (usesFieldRef()) {
@@ -227,6 +232,7 @@ public AsBuilder filter() {
 		 *
 		 * @return new instance of {@link IsArray}.
 		 */
+		@SuppressWarnings("NullAway")
 		public IsArray isArray() {
 
 			Assert.state(values == null, "Does it make sense to call isArray on an array; Maybe just skip it");
@@ -239,6 +245,7 @@ public IsArray isArray() {
 		 *
 		 * @return new instance of {@link Size}.
 		 */
+		@SuppressWarnings("NullAway")
 		public Size length() {
 
 			if (usesFieldRef()) {
@@ -253,6 +260,7 @@ public Size length() {
 		 *
 		 * @return new instance of {@link Slice}.
 		 */
+		@SuppressWarnings("NullAway")
 		public Slice slice() {
 
 			if (usesFieldRef()) {
@@ -269,6 +277,7 @@ public Slice slice() {
 		 * @param value must not be {@literal null}.
 		 * @return new instance of {@link IndexOfArray}.
 		 */
+		@SuppressWarnings("NullAway")
 		public IndexOfArray indexOf(Object value) {
 
 			if (usesFieldRef()) {
@@ -284,6 +293,7 @@ public IndexOfArray indexOf(Object value) {
 		 *
 		 * @return new instance of {@link ReverseArray}.
 		 */
+		@SuppressWarnings("NullAway")
 		public ReverseArray reverse() {
 
 			if (usesFieldRef()) {
@@ -301,6 +311,7 @@ public ReverseArray reverse() {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link ReduceInitialValueBuilder} to create {@link Reduce}.
 		 */
+		@SuppressWarnings("NullAway")
 		public ArrayOperatorFactory.ReduceInitialValueBuilder reduce(AggregationExpression expression) {
 
 			return initialValue -> (usesFieldRef() ? Reduce.arrayOf(fieldReference)
@@ -314,6 +325,7 @@ public ArrayOperatorFactory.ReduceInitialValueBuilder reduce(AggregationExpressi
 		 * @param expressions must not be {@literal null}.
 		 * @return new instance of {@link ReduceInitialValueBuilder} to create {@link Reduce}.
 		 */
+		@SuppressWarnings("NullAway")
 		public ArrayOperatorFactory.ReduceInitialValueBuilder reduce(PropertyExpression... expressions) {
 
 			return initialValue -> (usesFieldRef() ? Reduce.arrayOf(fieldReference) : Reduce.arrayOf(expression))
@@ -327,6 +339,7 @@ public ArrayOperatorFactory.ReduceInitialValueBuilder reduce(PropertyExpression.
 		 * @return new instance of {@link SortArray}.
 		 * @since 4.0
 		 */
+		@SuppressWarnings("NullAway")
 		public SortArray sort(Sort sort) {
 
 			if (usesFieldRef()) {
@@ -336,6 +349,23 @@ public SortArray sort(Sort sort) {
 			return (usesExpression() ? SortArray.sortArrayOf(expression) : SortArray.sortArray(values)).by(sort);
 		}
 
+		/**
+		 * Creates new {@link AggregationExpression} that takes the associated array and sorts it by the given
+		 * {@link Direction order}.
+		 *
+		 * @return new instance of {@link SortArray}.
+		 * @since 4.5
+		 */
+		@SuppressWarnings("NullAway")
+		public SortArray sort(Direction direction) {
+
+			if (usesFieldRef()) {
+				return SortArray.sortArrayOf(fieldReference).direction(direction);
+			}
+
+			return (usesExpression() ? SortArray.sortArrayOf(expression) : SortArray.sortArray(values)).direction(direction);
+		}
+
 		/**
 		 * Creates new {@link AggregationExpression} that transposes an array of input arrays so that the first element of
 		 * the output array would be an array containing, the first element of the first input array, the first element of
@@ -344,6 +374,7 @@ public SortArray sort(Sort sort) {
 		 * @param arrays must not be {@literal null}.
 		 * @return new instance of {@link Zip}.
 		 */
+		@SuppressWarnings("NullAway")
 		public Zip zipWith(Object... arrays) {
 
 			if (usesFieldRef()) {
@@ -360,6 +391,7 @@ public Zip zipWith(Object... arrays) {
 		 * @param value must not be {@literal null}.
 		 * @return new instance of {@link In}.
 		 */
+		@SuppressWarnings("NullAway")
 		public In containsValue(Object value) {
 
 			if (usesFieldRef()) {
@@ -376,6 +408,7 @@ public In containsValue(Object value) {
 		 * @return new instance of {@link ArrayToObject}.
 		 * @since 2.1
 		 */
+		@SuppressWarnings("NullAway")
 		public ArrayToObject toObject() {
 
 			if (usesFieldRef()) {
@@ -392,6 +425,7 @@ public ArrayToObject toObject() {
 		 * @return new instance of {@link First}.
 		 * @since 3.4
 		 */
+		@SuppressWarnings("NullAway")
 		public First first() {
 
 			if (usesFieldRef()) {
@@ -408,6 +442,7 @@ public First first() {
 		 * @return new instance of {@link Last}.
 		 * @since 3.4
 		 */
+		@SuppressWarnings("NullAway")
 		public Last last() {
 
 			if (usesFieldRef()) {
@@ -506,6 +541,7 @@ public static ArrayElemAt arrayOf(Collection<?> values) {
 		 * @param index the index number
 		 * @return new instance of {@link ArrayElemAt}.
 		 */
+		@Contract("_ -> new")
 		public ArrayElemAt elementAt(int index) {
 			return new ArrayElemAt(append(index));
 		}
@@ -516,6 +552,7 @@ public ArrayElemAt elementAt(int index) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link ArrayElemAt}.
 		 */
+		@Contract("_ -> new")
 		public ArrayElemAt elementAt(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -528,6 +565,7 @@ public ArrayElemAt elementAt(AggregationExpression expression) {
 		 * @param arrayFieldReference the field name.
 		 * @return new instance of {@link ArrayElemAt}.
 		 */
+		@Contract("_ -> new")
 		public ArrayElemAt elementAt(String arrayFieldReference) {
 
 			Assert.notNull(arrayFieldReference, "ArrayReference must not be null");
@@ -594,6 +632,7 @@ public static ConcatArrays arrayOf(Collection<?> values) {
 		 * @param arrayFieldReference must not be {@literal null}.
 		 * @return new instance of {@link ConcatArrays}.
 		 */
+		@Contract("_ -> new")
 		public ConcatArrays concat(String arrayFieldReference) {
 
 			Assert.notNull(arrayFieldReference, "ArrayFieldReference must not be null");
@@ -606,6 +645,7 @@ public ConcatArrays concat(String arrayFieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link ConcatArrays}.
 		 */
+		@Contract("_ -> new")
 		public ConcatArrays concat(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -681,9 +721,12 @@ public static AsBuilder filter(List<?> values) {
 
 		@Override
 		public Document toDocument(final AggregationOperationContext context) {
+
+			Assert.notNull(as, "As must be set first");
 			return toFilter(ExposedFields.from(as), context);
 		}
 
+		@SuppressWarnings("NullAway")
 		private Document toFilter(ExposedFields exposedFields, AggregationOperationContext context) {
 
 			Document filterExpression = new Document();
@@ -697,7 +740,7 @@ private Document toFilter(ExposedFields exposedFields, AggregationOperationConte
 			return new Document("$filter", filterExpression);
 		}
 
-		private Object getMappedInput(AggregationOperationContext context) {
+		private @Nullable Object getMappedInput(AggregationOperationContext context) {
 
 			if (input instanceof Field field) {
 				return context.getReference(field).toString();
@@ -710,7 +753,7 @@ private Object getMappedInput(AggregationOperationContext context) {
 			return input;
 		}
 
-		private Object getMappedCondition(AggregationOperationContext context) {
+		private @Nullable Object getMappedCondition(AggregationOperationContext context) {
 
 			if (!(condition instanceof AggregationExpression aggregationExpression)) {
 				return condition;
@@ -817,6 +860,7 @@ public static InputBuilder newBuilder() {
 			}
 
 			@Override
+			@Contract("_ -> this")
 			public AsBuilder filter(List<?> array) {
 
 				Assert.notNull(array, "Array must not be null");
@@ -825,6 +869,7 @@ public AsBuilder filter(List<?> array) {
 			}
 
 			@Override
+			@Contract("_ -> this")
 			public AsBuilder filter(Field field) {
 
 				Assert.notNull(field, "Field must not be null");
@@ -833,6 +878,7 @@ public AsBuilder filter(Field field) {
 			}
 
 			@Override
+			@Contract("_ -> this")
 			public AsBuilder filter(AggregationExpression expression) {
 
 				Assert.notNull(expression, "Expression must not be null");
@@ -841,6 +887,7 @@ public AsBuilder filter(AggregationExpression expression) {
 			}
 
 			@Override
+			@Contract("_ -> this")
 			public ConditionBuilder as(String variableName) {
 
 				Assert.notNull(variableName, "Variable name  must not be null");
@@ -1028,6 +1075,7 @@ public static Slice sliceArrayOf(Collection<?> values) {
 		 * @param count number of elements to slice.
 		 * @return new instance of {@link Slice}.
 		 */
+		@Contract("_ -> new")
 		public Slice itemCount(int count) {
 			return new Slice(append(count));
 		}
@@ -1040,6 +1088,7 @@ public Slice itemCount(int count) {
 		 * @return new instance of {@link Slice}.
 		 * @since 4.5
 		 */
+		@Contract("_ -> new")
 		public Slice itemCount(AggregationExpression count) {
 			return new Slice(append(count));
 		}
@@ -1158,6 +1207,7 @@ public static IndexOfArrayBuilder arrayOf(Collection<?> values) {
 		 * @param range the lookup range.
 		 * @return new instance of {@link IndexOfArray}.
 		 */
+		@Contract("_ -> new")
 		public IndexOfArray within(Range<Long> range) {
 			return new IndexOfArray(append(AggregationUtils.toRangeValues(range)));
 		}
@@ -1233,6 +1283,7 @@ public static RangeOperatorBuilder rangeStartingAt(long value) {
 			return new RangeOperatorBuilder(value);
 		}
 
+		@Contract("_ -> new")
 		public RangeOperator withStepSize(long stepSize) {
 			return new RangeOperator(append(stepSize));
 		}
@@ -1365,6 +1416,7 @@ public Document toDocument(AggregationOperationContext context) {
 			return new Document("$reduce", document);
 		}
 
+		@SuppressWarnings("NullAway")
 		private Object getMappedValue(Object value, AggregationOperationContext context) {
 
 			if (value instanceof Document) {
@@ -1684,6 +1736,7 @@ public static ZipBuilder arrayOf(Collection<?> values) {
 		 *
 		 * @return new instance of {@link Zip}.
 		 */
+		@Contract("-> new")
 		public Zip useLongestLength() {
 			return new Zip(append("useLongestLength", true));
 		}
@@ -1694,6 +1747,7 @@ public Zip useLongestLength() {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link Zip}.
 		 */
+		@Contract("_ -> new")
 		public Zip defaultTo(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -1706,6 +1760,7 @@ public Zip defaultTo(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Zip}.
 		 */
+		@Contract("_ -> new")
 		public Zip defaultTo(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -1718,6 +1773,7 @@ public Zip defaultTo(AggregationExpression expression) {
 		 * @param array must not be {@literal null}.
 		 * @return new instance of {@link Zip}.
 		 */
+		@Contract("_ -> new")
 		public Zip defaultTo(Object[] array) {
 
 			Assert.notNull(array, "Array must not be null");
@@ -1943,10 +1999,6 @@ public static First firstOf(AggregationExpression expression) {
 			return new First(expression);
 		}
 
-		/*
-		 * (non-Javadoc)
-		 * @see org.springframework.data.mongodb.core.aggregation.AbstractAggregationExpression#getMongoMethod()
-		 */
 		@Override
 		protected String getMongoMethod() {
 			return "$first";
@@ -1997,10 +2049,6 @@ public static Last lastOf(AggregationExpression expression) {
 			return new Last(expression);
 		}
 
-		/*
-		 * (non-Javadoc)
-		 * @see org.springframework.data.mongodb.core.aggregation.AbstractAggregationExpression#getMongoMethod()
-		 */
 		@Override
 		protected String getMongoMethod() {
 			return "$last";
@@ -2055,14 +2103,44 @@ public static SortArray sortArrayOf(AggregationExpression expression) {
 		 * @param sort must not be {@literal null}.
 		 * @return new instance of {@link SortArray}.
 		 */
+		@Contract("_ -> new")
 		public SortArray by(Sort sort) {
 			return new SortArray(append("sortBy", sort));
 		}
 
-		/*
-		 * (non-Javadoc)
-		 * @see org.springframework.data.mongodb.core.aggregation.AbstractAggregationExpression#getMongoMethod()
+		/**
+		 * Order the values for the array in the given direction.
+		 *
+		 * @param direction must not be {@literal null}.
+		 * @return new instance of {@link SortArray}.
+		 * @since 4.5
+		 */
+		public SortArray direction(Direction direction) {
+			return new SortArray(append("sortBy", direction.isAscending() ? 1 : -1));
+		}
+
+		/**
+		 * Sort the array elements by their values in ascending order. Suitable for arrays of simple types (e.g., integers,
+		 * strings).
+		 *
+		 * @return new instance of {@link SortArray}.
+		 * @since 4.5
 		 */
+		public SortArray byValueAscending() {
+			return direction(Direction.ASC);
+		}
+
+		/**
+		 * Sort the array elements by their values in descending order. Suitable for arrays of simple types (e.g., integers,
+		 * strings).
+		 *
+		 * @return new instance of {@link SortArray}.
+		 * @since 4.5
+		 */
+		public SortArray byValueDescending() {
+			return direction(Direction.DESC);
+		}
+
 		@Override
 		protected String getMongoMethod() {
 			return "$sortArray";
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BooleanOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BooleanOperators.java
index 69689908c9..f3ffdb7ad1 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BooleanOperators.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BooleanOperators.java
@@ -19,6 +19,8 @@
 import java.util.Collections;
 import java.util.List;
 
+import org.jspecify.annotations.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 
 /**
@@ -77,8 +79,8 @@ public static Not not(AggregationExpression expression) {
 	 */
 	public static class BooleanOperatorFactory {
 
-		private final String fieldReference;
-		private final AggregationExpression expression;
+		private final @Nullable String fieldReference;
+		private final @Nullable AggregationExpression expression;
 
 		/**
 		 * Creates new {@link BooleanOperatorFactory} for given {@literal fieldReference}.
@@ -130,6 +132,7 @@ public And and(String fieldReference) {
 			return createAnd().andField(fieldReference);
 		}
 
+		@SuppressWarnings("NullAway")
 		private And createAnd() {
 			return usesFieldRef() ? And.and(Fields.field(fieldReference)) : And.and(expression);
 		}
@@ -160,6 +163,7 @@ public Or or(String fieldReference) {
 			return createOr().orField(fieldReference);
 		}
 
+		@SuppressWarnings("NullAway")
 		private Or createOr() {
 			return usesFieldRef() ? Or.or(Fields.field(fieldReference)) : Or.or(expression);
 		}
@@ -169,6 +173,7 @@ private Or createOr() {
 		 *
 		 * @return new instance of {@link Not}.
 		 */
+		@SuppressWarnings("NullAway")
 		public Not not() {
 			return usesFieldRef() ? Not.not(fieldReference) : Not.not(expression);
 		}
@@ -211,6 +216,7 @@ public static And and(Object... expressions) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link And}.
 		 */
+		@Contract("_ -> new")
 		public And andExpression(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -223,6 +229,7 @@ public And andExpression(AggregationExpression expression) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link And}.
 		 */
+		@Contract("_ -> new")
 		public And andField(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -235,6 +242,7 @@ public And andField(String fieldReference) {
 		 * @param value must not be {@literal null}.
 		 * @return new instance of {@link And}.
 		 */
+		@Contract("_ -> new")
 		public And andValue(Object value) {
 
 			Assert.notNull(value, "Value must not be null");
@@ -277,6 +285,7 @@ public static Or or(Object... expressions) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Or}.
 		 */
+		@Contract("_ -> new")
 		public Or orExpression(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -289,6 +298,7 @@ public Or orExpression(AggregationExpression expression) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link Or}.
 		 */
+		@Contract("_ -> new")
 		public Or orField(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -301,6 +311,7 @@ public Or orField(String fieldReference) {
 		 * @param value must not be {@literal null}.
 		 * @return new instance of {@link Or}.
 		 */
+		@Contract("_ -> new")
 		public Or orValue(Object value) {
 
 			Assert.notNull(value, "Value must not be null");
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketAutoOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketAutoOperation.java
index 36492e2a81..16eca4ec22 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketAutoOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketAutoOperation.java
@@ -16,6 +16,7 @@
 package org.springframework.data.mongodb.core.aggregation;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.BucketAutoOperation.BucketAutoOperationOutputBuilder;
 import org.springframework.data.mongodb.core.aggregation.BucketOperationSupport.OutputBuilder;
 import org.springframework.util.Assert;
@@ -38,7 +39,7 @@ public class BucketAutoOperation extends BucketOperationSupport<BucketAutoOperat
 		implements FieldsExposingAggregationOperation {
 
 	private final int buckets;
-	private final String granularity;
+	private final @Nullable String granularity;
 
 	/**
 	 * Creates a new {@link BucketAutoOperation} given a {@link Field group-by field}.
@@ -80,7 +81,7 @@ private BucketAutoOperation(BucketAutoOperation bucketOperation, Outputs outputs
 		this.granularity = bucketOperation.granularity;
 	}
 
-	private BucketAutoOperation(BucketAutoOperation bucketOperation, int buckets, String granularity) {
+	private BucketAutoOperation(BucketAutoOperation bucketOperation, int buckets, @Nullable String granularity) {
 
 		super(bucketOperation);
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketOperation.java
index 525789e628..6ed686c086 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketOperation.java
@@ -21,7 +21,9 @@
 import java.util.List;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.BucketOperation.BucketOperationOutputBuilder;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 
 /**
@@ -40,7 +42,7 @@ public class BucketOperation extends BucketOperationSupport<BucketOperation, Buc
 		implements FieldsExposingAggregationOperation {
 
 	private final List<Object> boundaries;
-	private final Object defaultBucket;
+	private final @Nullable Object defaultBucket;
 
 	/**
 	 * Creates a new {@link BucketOperation} given a {@link Field group-by field}.
@@ -76,7 +78,7 @@ private BucketOperation(BucketOperation bucketOperation, Outputs outputs) {
 		this.defaultBucket = bucketOperation.defaultBucket;
 	}
 
-	private BucketOperation(BucketOperation bucketOperation, List<Object> boundaries, Object defaultBucket) {
+	private BucketOperation(BucketOperation bucketOperation, List<Object> boundaries, @Nullable Object defaultBucket) {
 
 		super(bucketOperation);
 
@@ -111,6 +113,7 @@ public String getOperator() {
 	 * @param literal must not be {@literal null}.
 	 * @return new instance of {@link BucketOperation}.
 	 */
+	@Contract("_ -> new")
 	public BucketOperation withDefaultBucket(Object literal) {
 
 		Assert.notNull(literal, "Default bucket literal must not be null");
@@ -124,6 +127,7 @@ public BucketOperation withDefaultBucket(Object literal) {
 	 * @param boundaries must not be {@literal null}.
 	 * @return new instance of {@link BucketOperation}.
 	 */
+	@Contract("_ -> new")
 	public BucketOperation withBoundaries(Object... boundaries) {
 
 		Assert.notNull(boundaries, "Boundaries must not be null");
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketOperationSupport.java
index e19ad59a3f..3d5ded05c2 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketOperationSupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketOperationSupport.java
@@ -22,6 +22,7 @@
 import java.util.List;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.BucketOperationSupport.OutputBuilder;
 import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
 import org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder;
@@ -40,8 +41,8 @@
 public abstract class BucketOperationSupport<T extends BucketOperationSupport<T, B>, B extends OutputBuilder<B, T>>
 		implements FieldsExposingAggregationOperation {
 
-	private final Field groupByField;
-	private final AggregationExpression groupByExpression;
+	private final @Nullable Field groupByField;
+	private final @Nullable AggregationExpression groupByExpression;
 	private final Outputs outputs;
 
 	/**
@@ -142,12 +143,17 @@ public Document toDocument(AggregationOperationContext context) {
 	}
 
 	@Override
+
 	public Document toDocument(AggregationOperationContext context) {
 
 		Document document = new Document();
 
-		document.put("groupBy", groupByExpression == null ? context.getReference(groupByField).toString()
-				: groupByExpression.toDocument(context));
+		if(groupByExpression != null) {
+			document.put("groupBy", groupByExpression.toDocument(context));
+		} else if (groupByField != null) {
+			document.put("groupBy", context.getReference(groupByField).toString());
+
+		}
 
 		if (!outputs.isEmpty()) {
 			document.put("output", outputs.toDocument(context));
@@ -625,7 +631,9 @@ public SpelExpressionOutput(String expression, Object[] parameters) {
 
 		@Override
 		public Document toDocument(AggregationOperationContext context) {
-			return (Document) TRANSFORMER.transform(expression, context, params);
+
+			Object o =  TRANSFORMER.transform(expression, context, params);
+			return o instanceof Document document ? document : new Document();
 		}
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ComparisonOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ComparisonOperators.java
index f27b7f16cb..e2626c3a16 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ComparisonOperators.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ComparisonOperators.java
@@ -18,6 +18,8 @@
 import java.util.Collections;
 import java.util.List;
 
+import org.jspecify.annotations.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 
 /**
@@ -50,8 +52,8 @@ public static ComparisonOperatorFactory valueOf(AggregationExpression expression
 
 	public static class ComparisonOperatorFactory {
 
-		private final String fieldReference;
-		private final AggregationExpression expression;
+		private final @Nullable String fieldReference;
+		private final @Nullable AggregationExpression expression;
 
 		/**
 		 * Creates new {@link ComparisonOperatorFactory} for given {@literal fieldReference}.
@@ -107,6 +109,7 @@ public Cmp compareToValue(Object value) {
 			return createCmp().compareToValue(value);
 		}
 
+		@SuppressWarnings("NullAway")
 		private Cmp createCmp() {
 			return usesFieldRef() ? Cmp.valueOf(fieldReference) : Cmp.valueOf(expression);
 		}
@@ -144,6 +147,7 @@ public Eq equalToValue(Object value) {
 			return createEq().equalToValue(value);
 		}
 
+		@SuppressWarnings("NullAway")
 		private Eq createEq() {
 			return usesFieldRef() ? Eq.valueOf(fieldReference) : Eq.valueOf(expression);
 		}
@@ -181,6 +185,7 @@ public Gt greaterThanValue(Object value) {
 			return createGt().greaterThanValue(value);
 		}
 
+		@SuppressWarnings("NullAway")
 		private Gt createGt() {
 			return usesFieldRef() ? Gt.valueOf(fieldReference) : Gt.valueOf(expression);
 		}
@@ -218,6 +223,7 @@ public Gte greaterThanEqualToValue(Object value) {
 			return createGte().greaterThanEqualToValue(value);
 		}
 
+		@SuppressWarnings("NullAway")
 		private Gte createGte() {
 			return usesFieldRef() ? Gte.valueOf(fieldReference) : Gte.valueOf(expression);
 		}
@@ -255,6 +261,7 @@ public Lt lessThanValue(Object value) {
 			return createLt().lessThanValue(value);
 		}
 
+		@SuppressWarnings("NullAway")
 		private Lt createLt() {
 			return usesFieldRef() ? Lt.valueOf(fieldReference) : Lt.valueOf(expression);
 		}
@@ -292,6 +299,7 @@ public Lte lessThanEqualToValue(Object value) {
 			return createLte().lessThanEqualToValue(value);
 		}
 
+		@SuppressWarnings("NullAway")
 		private Lte createLte() {
 			return usesFieldRef() ? Lte.valueOf(fieldReference) : Lte.valueOf(expression);
 		}
@@ -329,6 +337,7 @@ public Ne notEqualToValue(Object value) {
 			return createNe().notEqualToValue(value);
 		}
 
+		@SuppressWarnings("NullAway")
 		private Ne createNe() {
 			return usesFieldRef() ? Ne.valueOf(fieldReference) : Ne.valueOf(expression);
 		}
@@ -384,6 +393,7 @@ public static Cmp valueOf(AggregationExpression expression) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link Cmp}.
 		 */
+		@Contract("_ -> new")
 		public Cmp compareTo(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -396,6 +406,7 @@ public Cmp compareTo(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Cmp}.
 		 */
+		@Contract("_ -> new")
 		public Cmp compareTo(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -408,6 +419,7 @@ public Cmp compareTo(AggregationExpression expression) {
 		 * @param value must not be {@literal null}.
 		 * @return new instance of {@link Cmp}.
 		 */
+		@Contract("_ -> new")
 		public Cmp compareToValue(Object value) {
 
 			Assert.notNull(value, "Value must not be null");
@@ -461,6 +473,7 @@ public static Eq valueOf(AggregationExpression expression) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link Eq}.
 		 */
+		@Contract("_ -> new")
 		public Eq equalTo(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -473,6 +486,7 @@ public Eq equalTo(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Eq}.
 		 */
+		@Contract("_ -> new")
 		public Eq equalTo(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -485,6 +499,7 @@ public Eq equalTo(AggregationExpression expression) {
 		 * @param value must not be {@literal null}.
 		 * @return new instance of {@link Eq}.
 		 */
+		@Contract("_ -> new")
 		public Eq equalToValue(Object value) {
 
 			Assert.notNull(value, "Value must not be null");
@@ -538,6 +553,7 @@ public static Gt valueOf(AggregationExpression expression) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link Gt}.
 		 */
+		@Contract("_ -> new")
 		public Gt greaterThan(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -550,6 +566,7 @@ public Gt greaterThan(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Gt}.
 		 */
+		@Contract("_ -> new")
 		public Gt greaterThan(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -562,6 +579,7 @@ public Gt greaterThan(AggregationExpression expression) {
 		 * @param value must not be {@literal null}.
 		 * @return new instance of {@link Gt}.
 		 */
+		@Contract("_ -> new")
 		public Gt greaterThanValue(Object value) {
 
 			Assert.notNull(value, "Value must not be null");
@@ -615,6 +633,7 @@ public static Lt valueOf(AggregationExpression expression) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link Lt}.
 		 */
+		@Contract("_ -> new")
 		public Lt lessThan(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -627,6 +646,7 @@ public Lt lessThan(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Lt}.
 		 */
+		@Contract("_ -> new")
 		public Lt lessThan(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -639,6 +659,7 @@ public Lt lessThan(AggregationExpression expression) {
 		 * @param value must not be {@literal null}.
 		 * @return new instance of {@link Lt}.
 		 */
+		@Contract("_ -> new")
 		public Lt lessThanValue(Object value) {
 
 			Assert.notNull(value, "Value must not be null");
@@ -692,6 +713,7 @@ public static Gte valueOf(AggregationExpression expression) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link Gte}.
 		 */
+		@Contract("_ -> new")
 		public Gte greaterThanEqualTo(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -704,6 +726,7 @@ public Gte greaterThanEqualTo(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Gte}.
 		 */
+		@Contract("_ -> new")
 		public Gte greaterThanEqualTo(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -716,6 +739,7 @@ public Gte greaterThanEqualTo(AggregationExpression expression) {
 		 * @param value must not be {@literal null}.
 		 * @return new instance of {@link Gte}.
 		 */
+		@Contract("_ -> new")
 		public Gte greaterThanEqualToValue(Object value) {
 
 			Assert.notNull(value, "Value must not be null");
@@ -769,6 +793,7 @@ public static Lte valueOf(AggregationExpression expression) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link Lte}.
 		 */
+		@Contract("_ -> new")
 		public Lte lessThanEqualTo(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -781,6 +806,7 @@ public Lte lessThanEqualTo(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Lte}.
 		 */
+		@Contract("_ -> new")
 		public Lte lessThanEqualTo(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -793,6 +819,7 @@ public Lte lessThanEqualTo(AggregationExpression expression) {
 		 * @param value must not be {@literal null}.
 		 * @return new instance of {@link Lte}.
 		 */
+		@Contract("_ -> new")
 		public Lte lessThanEqualToValue(Object value) {
 
 			Assert.notNull(value, "Value must not be null");
@@ -846,6 +873,7 @@ public static Ne valueOf(AggregationExpression expression) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link Ne}.
 		 */
+		@Contract("_ -> new")
 		public Ne notEqualTo(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -858,6 +886,7 @@ public Ne notEqualTo(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Ne}.
 		 */
+		@Contract("_ -> new")
 		public Ne notEqualTo(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -870,6 +899,7 @@ public Ne notEqualTo(AggregationExpression expression) {
 		 * @param value must not be {@literal null}.
 		 * @return new instance of {@link Ne}.
 		 */
+		@Contract("_ -> new")
 		public Ne notEqualToValue(Object value) {
 
 			Assert.notNull(value, "Value must not be null");
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperators.java
index 323a11895b..462d94d6f1 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperators.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperators.java
@@ -22,12 +22,13 @@
 import java.util.List;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.dao.InvalidDataAccessApiUsageException;
 import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.Cond.OtherwiseBuilder;
 import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.Cond.ThenBuilder;
 import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.Switch.CaseOperator;
 import org.springframework.data.mongodb.core.query.CriteriaDefinition;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 
@@ -211,6 +212,7 @@ public OtherwiseBuilder thenValueOf(String fieldReference) {
 			return createThenBuilder().thenValueOf(fieldReference);
 		}
 
+		@SuppressWarnings("NullAway")
 		private ThenBuilder createThenBuilder() {
 
 			if (usesFieldRef()) {
@@ -303,6 +305,7 @@ private Object mapCondition(Object condition, AggregationOperationContext contex
 			}
 		}
 
+		@SuppressWarnings("NullAway")
 		private Object resolve(Object value, AggregationOperationContext context) {
 
 			if (value instanceof Field field) {
@@ -389,7 +392,7 @@ public interface ThenBuilder extends OrBuilder {
 		 */
 		static final class IfNullOperatorBuilder implements IfNullBuilder, ThenBuilder {
 
-			private @Nullable List<Object> conditions;
+			private List<Object> conditions;
 
 			private IfNullOperatorBuilder() {
 				conditions = new ArrayList<>();
@@ -404,6 +407,7 @@ public static IfNullOperatorBuilder newBuilder() {
 				return new IfNullOperatorBuilder();
 			}
 
+			@Contract("_ -> this")
 			public ThenBuilder ifNull(String fieldReference) {
 
 				Assert.hasText(fieldReference, "FieldReference name must not be null or empty");
@@ -412,6 +416,7 @@ public ThenBuilder ifNull(String fieldReference) {
 			}
 
 			@Override
+			@Contract("_ -> this")
 			public ThenBuilder ifNull(AggregationExpression expression) {
 
 				Assert.notNull(expression, "AggregationExpression name must not be null or empty");
@@ -420,25 +425,30 @@ public ThenBuilder ifNull(AggregationExpression expression) {
 			}
 
 			@Override
+			@Contract("_ -> this")
 			public ThenBuilder orIfNull(String fieldReference) {
 				return ifNull(fieldReference);
 			}
 
 			@Override
+			@Contract("_ -> this")
 			public ThenBuilder orIfNull(AggregationExpression expression) {
 				return ifNull(expression);
 			}
 
+			@Contract("_ -> new")
 			public IfNull then(Object value) {
 				return new IfNull(conditions, value);
 			}
 
+			@Contract("_ -> new")
 			public IfNull thenValueOf(String fieldReference) {
 
 				Assert.notNull(fieldReference, "FieldReference must not be null");
 				return new IfNull(conditions, Fields.field(fieldReference));
 			}
 
+			@Contract("_ -> new")
 			public IfNull thenValueOf(AggregationExpression expression) {
 
 				Assert.notNull(expression, "Expression must not be null");
@@ -491,6 +501,7 @@ public static Switch switchCases(List<CaseOperator> conditions) {
 		 * @param value must not be {@literal null}.
 		 * @return new instance of {@link Switch}.
 		 */
+		@Contract("_ -> new")
 		public Switch defaultTo(Object value) {
 			return new Switch(append("default", value));
 		}
@@ -623,6 +634,7 @@ public Document toDocument(AggregationOperationContext context) {
 			return new Document("$cond", condObject);
 		}
 
+		@SuppressWarnings("NullAway")
 		private Object resolveValue(AggregationOperationContext context, Object value) {
 
 			if (value instanceof Document || value instanceof Field) {
@@ -886,6 +898,7 @@ public static ConditionalExpressionBuilder newBuilder() {
 			}
 
 			@Override
+			@Contract("_ -> this")
 			public ConditionalExpressionBuilder when(Document booleanExpression) {
 
 				Assert.notNull(booleanExpression, "'Boolean expression' must not be null");
@@ -895,6 +908,7 @@ public ConditionalExpressionBuilder when(Document booleanExpression) {
 			}
 
 			@Override
+			@Contract("_ -> this")
 			public ThenBuilder when(CriteriaDefinition criteria) {
 
 				Assert.notNull(criteria, "Criteria must not be null");
@@ -903,6 +917,7 @@ public ThenBuilder when(CriteriaDefinition criteria) {
 			}
 
 			@Override
+			@Contract("_ -> this")
 			public ThenBuilder when(AggregationExpression expression) {
 
 				Assert.notNull(expression, "AggregationExpression field must not be null");
@@ -911,6 +926,7 @@ public ThenBuilder when(AggregationExpression expression) {
 			}
 
 			@Override
+			@Contract("_ -> this")
 			public ThenBuilder when(String booleanField) {
 
 				Assert.hasText(booleanField, "Boolean field name must not be null or empty");
@@ -919,6 +935,7 @@ public ThenBuilder when(String booleanField) {
 			}
 
 			@Override
+			@Contract("_ -> this")
 			public OtherwiseBuilder then(Object thenValue) {
 
 				Assert.notNull(thenValue, "Then-value must not be null");
@@ -927,6 +944,7 @@ public OtherwiseBuilder then(Object thenValue) {
 			}
 
 			@Override
+			@Contract("_ -> this")
 			public OtherwiseBuilder thenValueOf(String fieldReference) {
 
 				Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -935,6 +953,7 @@ public OtherwiseBuilder thenValueOf(String fieldReference) {
 			}
 
 			@Override
+			@Contract("_ -> this")
 			public OtherwiseBuilder thenValueOf(AggregationExpression expression) {
 
 				Assert.notNull(expression, "AggregationExpression must not be null");
@@ -943,23 +962,32 @@ public OtherwiseBuilder thenValueOf(AggregationExpression expression) {
 			}
 
 			@Override
+			@Contract("_ -> new")
 			public Cond otherwise(Object otherwiseValue) {
 
 				Assert.notNull(otherwiseValue, "Value must not be null");
+				Assert.notNull(condition, "Condition value needs to be set first");
+				Assert.notNull(thenValue, "Then value needs to be set first");
 				return new Cond(condition, thenValue, otherwiseValue);
 			}
 
 			@Override
+			@Contract("_ -> new")
 			public Cond otherwiseValueOf(String fieldReference) {
 
 				Assert.notNull(fieldReference, "FieldReference must not be null");
+				Assert.notNull(condition, "Condition value needs to be set first");
+				Assert.notNull(thenValue, "Then value needs to be set first");
 				return new Cond(condition, thenValue, Fields.field(fieldReference));
 			}
 
 			@Override
+			@Contract("_ -> new")
 			public Cond otherwiseValueOf(AggregationExpression expression) {
 
 				Assert.notNull(expression, "AggregationExpression must not be null");
+				Assert.notNull(condition, "Condition value needs to be set first");
+				Assert.notNull(thenValue, "Then value needs to be set first");
 				return new Cond(condition, thenValue, expression);
 			}
 		}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConvertOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConvertOperators.java
index aa085b2a29..35a6ad061c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConvertOperators.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConvertOperators.java
@@ -17,8 +17,8 @@
 
 import java.util.Collections;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
 /**
@@ -242,10 +242,12 @@ public DegreesToRadians convertDegreesToRadians() {
 			return DegreesToRadians.degreesToRadians(valueObject());
 		}
 
+		@SuppressWarnings("NullAway")
 		private Convert createConvert() {
 			return usesFieldRef() ? Convert.convertValueOf(fieldReference) : Convert.convertValueOf(expression);
 		}
 
+		@SuppressWarnings("NullAway")
 		private Object valueObject() {
 			return usesFieldRef() ? Fields.field(fieldReference) : expression;
 		}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java
index ff6ed7e983..7bf8a231ff 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java
@@ -26,7 +26,8 @@
 import java.util.TimeZone;
 import java.util.concurrent.TimeUnit;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
@@ -848,6 +849,7 @@ public TsSecond tsSecond() {
 			return TsSecond.tsSecond(dateReference());
 		}
 
+		@SuppressWarnings("NullAway")
 		private Object dateReference() {
 
 			if (usesFieldRef()) {
@@ -1076,6 +1078,7 @@ public static DayOfYear dayOfYear(AggregationExpression expression) {
 		 * @since 2.1
 		 */
 		@Override
+		@Contract("_ -> new")
 		public DayOfYear withTimezone(Timezone timezone) {
 
 			Assert.notNull(timezone, "Timezone must not be null");
@@ -1148,6 +1151,7 @@ public static DayOfMonth dayOfMonth(AggregationExpression expression) {
 		 * @since 2.1
 		 */
 		@Override
+		@Contract("_ -> new")
 		public DayOfMonth withTimezone(Timezone timezone) {
 
 			Assert.notNull(timezone, "Timezone must not be null");
@@ -1220,6 +1224,7 @@ public static DayOfWeek dayOfWeek(AggregationExpression expression) {
 		 * @since 2.1
 		 */
 		@Override
+		@Contract("_ -> new")
 		public DayOfWeek withTimezone(Timezone timezone) {
 
 			Assert.notNull(timezone, "Timezone must not be null");
@@ -1292,6 +1297,7 @@ public static Year yearOf(AggregationExpression expression) {
 		 * @since 2.1
 		 */
 		@Override
+		@Contract("_ -> new")
 		public Year withTimezone(Timezone timezone) {
 
 			Assert.notNull(timezone, "Timezone must not be null");
@@ -1364,6 +1370,7 @@ public static Month monthOf(AggregationExpression expression) {
 		 * @since 2.1
 		 */
 		@Override
+		@Contract("_ -> new")
 		public Month withTimezone(Timezone timezone) {
 
 			Assert.notNull(timezone, "Timezone must not be null");
@@ -1436,6 +1443,7 @@ public static Week weekOf(AggregationExpression expression) {
 		 * @since 2.1
 		 */
 		@Override
+		@Contract("_ -> new")
 		public Week withTimezone(Timezone timezone) {
 
 			Assert.notNull(timezone, "Timezone must not be null");
@@ -1508,6 +1516,7 @@ public static Hour hourOf(AggregationExpression expression) {
 		 * @since 2.1
 		 */
 		@Override
+		@Contract("_ -> new")
 		public Hour withTimezone(Timezone timezone) {
 
 			Assert.notNull(timezone, "Timezone must not be null");
@@ -1580,6 +1589,7 @@ public static Minute minuteOf(AggregationExpression expression) {
 		 * @since 2.1
 		 */
 		@Override
+		@Contract("_ -> new")
 		public Minute withTimezone(Timezone timezone) {
 
 			Assert.notNull(timezone, "Timezone must not be null");
@@ -1652,6 +1662,7 @@ public static Second secondOf(AggregationExpression expression) {
 		 * @since 2.1
 		 */
 		@Override
+		@Contract("_ -> new")
 		public Second withTimezone(Timezone timezone) {
 
 			Assert.notNull(timezone, "Timezone must not be null");
@@ -1724,6 +1735,7 @@ public static Millisecond millisecondOf(AggregationExpression expression) {
 		 * @since 2.1
 		 */
 		@Override
+		@Contract("_ -> new")
 		public Millisecond withTimezone(Timezone timezone) {
 
 			Assert.notNull(timezone, "Timezone must not be null");
@@ -1810,6 +1822,7 @@ public static FormatBuilder dateOf(final AggregationExpression expression) {
 		 * @since 2.1
 		 */
 		@Override
+		@Contract("_ -> new")
 		public DateToString withTimezone(Timezone timezone) {
 
 			Assert.notNull(timezone, "Timezone must not be null");
@@ -1824,6 +1837,7 @@ public DateToString withTimezone(Timezone timezone) {
 		 * @return new instance of {@link DateToString}.
 		 * @since 2.1
 		 */
+		@Contract("_ -> new")
 		public DateToString onNullReturn(Object value) {
 			return new DateToString(append("onNull", value));
 		}
@@ -1836,6 +1850,7 @@ public DateToString onNullReturn(Object value) {
 		 * @return new instance of {@link DateToString}.
 		 * @since 2.1
 		 */
+		@Contract("_ -> new")
 		public DateToString onNullReturnValueOf(String fieldReference) {
 			return onNullReturn(Fields.field(fieldReference));
 		}
@@ -1848,6 +1863,7 @@ public DateToString onNullReturnValueOf(String fieldReference) {
 		 * @return new instance of {@link DateToString}.
 		 * @since 2.1
 		 */
+		@Contract("_ -> new")
 		public DateToString onNullReturnValueOf(AggregationExpression expression) {
 			return onNullReturn(expression);
 		}
@@ -1973,6 +1989,7 @@ public static IsoDayOfWeek isoDayOfWeek(AggregationExpression expression) {
 		 * @since 2.1
 		 */
 		@Override
+		@Contract("_ -> new")
 		public IsoDayOfWeek withTimezone(Timezone timezone) {
 
 			Assert.notNull(timezone, "Timezone must not be null");
@@ -2045,6 +2062,7 @@ public static IsoWeek isoWeekOf(AggregationExpression expression) {
 		 * @since 2.1
 		 */
 		@Override
+		@Contract("_ -> new")
 		public IsoWeek withTimezone(Timezone timezone) {
 
 			Assert.notNull(timezone, "Timezone must not be null");
@@ -2117,6 +2135,7 @@ public static IsoWeekYear isoWeekYearOf(AggregationExpression expression) {
 		 * @since 2.1
 		 */
 		@Override
+		@Contract("_ -> new")
 		public IsoWeekYear withTimezone(Timezone timezone) {
 
 			Assert.notNull(timezone, "Timezone must not be null");
@@ -2301,6 +2320,7 @@ public static DateFromPartsWithYear dateFromParts() {
 		 * @return new instance.
 		 * @throws IllegalArgumentException if given {@literal month} is {@literal null}.
 		 */
+		@Contract("_ -> new")
 		public DateFromParts month(Object month) {
 			return new DateFromParts(append("month", month));
 		}
@@ -2312,6 +2332,7 @@ public DateFromParts month(Object month) {
 		 * @return new instance.
 		 * @throws IllegalArgumentException if given {@literal fieldReference} is {@literal null}.
 		 */
+		@Contract("_ -> new")
 		public DateFromParts monthOf(String fieldReference) {
 			return month(Fields.field(fieldReference));
 		}
@@ -2323,6 +2344,7 @@ public DateFromParts monthOf(String fieldReference) {
 		 * @return new instance.
 		 * @throws IllegalArgumentException if given {@literal expression} is {@literal null}.
 		 */
+		@Contract("_ -> new")
 		public DateFromParts monthOf(AggregationExpression expression) {
 			return month(expression);
 		}
@@ -2335,6 +2357,7 @@ public DateFromParts monthOf(AggregationExpression expression) {
 		 * @return new instance.
 		 * @throws IllegalArgumentException if given {@literal day} is {@literal null}.
 		 */
+		@Contract("_ -> new")
 		public DateFromParts day(Object day) {
 			return new DateFromParts(append("day", day));
 		}
@@ -2346,6 +2369,7 @@ public DateFromParts day(Object day) {
 		 * @return new instance.
 		 * @throws IllegalArgumentException if given {@literal fieldReference} is {@literal null}.
 		 */
+		@Contract("_ -> new")
 		public DateFromParts dayOf(String fieldReference) {
 			return day(Fields.field(fieldReference));
 		}
@@ -2357,26 +2381,31 @@ public DateFromParts dayOf(String fieldReference) {
 		 * @return new instance.
 		 * @throws IllegalArgumentException if given {@literal expression} is {@literal null}.
 		 */
+		@Contract("_ -> new")
 		public DateFromParts dayOf(AggregationExpression expression) {
 			return day(expression);
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public DateFromParts hour(Object hour) {
 			return new DateFromParts(append("hour", hour));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public DateFromParts minute(Object minute) {
 			return new DateFromParts(append("minute", minute));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public DateFromParts second(Object second) {
 			return new DateFromParts(append("second", second));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public DateFromParts millisecond(Object millisecond) {
 			return new DateFromParts(append("millisecond", millisecond));
 		}
@@ -2390,6 +2419,7 @@ public DateFromParts millisecond(Object millisecond) {
 		 * @throws IllegalArgumentException if given {@literal timezone} is {@literal null}.
 		 */
 		@Override
+		@Contract("_ -> new")
 		public DateFromParts withTimezone(Timezone timezone) {
 			return new DateFromParts(appendTimezone(argumentMap(), timezone));
 		}
@@ -2477,6 +2507,7 @@ public static IsoDateFromPartsWithYear dateFromParts() {
 		 * @return new instance.
 		 * @throws IllegalArgumentException if given {@literal isoWeek} is {@literal null}.
 		 */
+		@Contract("_ -> new")
 		public IsoDateFromParts isoWeek(Object isoWeek) {
 			return new IsoDateFromParts(append("isoWeek", isoWeek));
 		}
@@ -2488,6 +2519,7 @@ public IsoDateFromParts isoWeek(Object isoWeek) {
 		 * @return new instance.
 		 * @throws IllegalArgumentException if given {@literal fieldReference} is {@literal null}.
 		 */
+		@Contract("_ -> new")
 		public IsoDateFromParts isoWeekOf(String fieldReference) {
 			return isoWeek(Fields.field(fieldReference));
 		}
@@ -2499,6 +2531,7 @@ public IsoDateFromParts isoWeekOf(String fieldReference) {
 		 * @return new instance.
 		 * @throws IllegalArgumentException if given {@literal expression} is {@literal null}.
 		 */
+		@Contract("_ -> new")
 		public IsoDateFromParts isoWeekOf(AggregationExpression expression) {
 			return isoWeek(expression);
 		}
@@ -2511,6 +2544,7 @@ public IsoDateFromParts isoWeekOf(AggregationExpression expression) {
 		 * @return new instance.
 		 * @throws IllegalArgumentException if given {@literal isoWeek} is {@literal null}.
 		 */
+		@Contract("_ -> new")
 		public IsoDateFromParts isoDayOfWeek(Object day) {
 			return new IsoDateFromParts(append("isoDayOfWeek", day));
 		}
@@ -2522,6 +2556,7 @@ public IsoDateFromParts isoDayOfWeek(Object day) {
 		 * @return new instance.
 		 * @throws IllegalArgumentException if given {@literal fieldReference} is {@literal null}.
 		 */
+		@Contract("_ -> new")
 		public IsoDateFromParts isoDayOfWeekOf(String fieldReference) {
 			return isoDayOfWeek(Fields.field(fieldReference));
 		}
@@ -2533,26 +2568,31 @@ public IsoDateFromParts isoDayOfWeekOf(String fieldReference) {
 		 * @return new instance.
 		 * @throws IllegalArgumentException if given {@literal expression} is {@literal null}.
 		 */
+		@Contract("_ -> new")
 		public IsoDateFromParts isoDayOfWeekOf(AggregationExpression expression) {
 			return isoDayOfWeek(expression);
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public IsoDateFromParts hour(Object hour) {
 			return new IsoDateFromParts(append("hour", hour));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public IsoDateFromParts minute(Object minute) {
 			return new IsoDateFromParts(append("minute", minute));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public IsoDateFromParts second(Object second) {
 			return new IsoDateFromParts(append("second", second));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public IsoDateFromParts millisecond(Object millisecond) {
 			return new IsoDateFromParts(append("millisecond", millisecond));
 		}
@@ -2566,6 +2606,7 @@ public IsoDateFromParts millisecond(Object millisecond) {
 		 * @throws IllegalArgumentException if given {@literal timezone} is {@literal null}.
 		 */
 		@Override
+		@Contract("_ -> new")
 		public IsoDateFromParts withTimezone(Timezone timezone) {
 			return new IsoDateFromParts(appendTimezone(argumentMap(), timezone));
 		}
@@ -2676,6 +2717,7 @@ public static DateToParts datePartsOf(AggregationExpression expression) {
 		 *
 		 * @return new instance of {@link DateToParts}.
 		 */
+		@Contract("_ -> new")
 		public DateToParts iso8601() {
 			return new DateToParts(append("iso8601", true));
 		}
@@ -2689,6 +2731,7 @@ public DateToParts iso8601() {
 		 * @throws IllegalArgumentException if given {@literal timezone} is {@literal null}.
 		 */
 		@Override
+		@Contract("_ -> new")
 		public DateToParts withTimezone(Timezone timezone) {
 			return new DateToParts(appendTimezone(argumentMap(), timezone));
 		}
@@ -2733,6 +2776,7 @@ public static DateFromString fromString(Object value) {
 		 * @return new instance of {@link DateFromString}.
 		 * @throws IllegalArgumentException if given {@literal fieldReference} is {@literal null}.
 		 */
+		@Contract("_ -> new")
 		public static DateFromString fromStringOf(String fieldReference) {
 			return fromString(Fields.field(fieldReference));
 		}
@@ -2744,6 +2788,7 @@ public static DateFromString fromStringOf(String fieldReference) {
 		 * @return new instance of {@link DateFromString}.
 		 * @throws IllegalArgumentException if given {@literal expression} is {@literal null}.
 		 */
+		@Contract("_ -> new")
 		public static DateFromString fromStringOf(AggregationExpression expression) {
 			return fromString(expression);
 		}
@@ -2757,6 +2802,7 @@ public static DateFromString fromStringOf(AggregationExpression expression) {
 		 * @throws IllegalArgumentException if given {@literal timezone} is {@literal null}.
 		 */
 		@Override
+		@Contract("_ -> new")
 		public DateFromString withTimezone(Timezone timezone) {
 			return new DateFromString(appendTimezone(argumentMap(), timezone));
 		}
@@ -2769,6 +2815,7 @@ public DateFromString withTimezone(Timezone timezone) {
 		 * @return new instance of {@link DateFromString}.
 		 * @throws IllegalArgumentException if given {@literal format} is {@literal null}.
 		 */
+		@Contract("_ -> new")
 		public DateFromString withFormat(String format) {
 
 			Assert.notNull(format, "Format must not be null");
@@ -2838,6 +2885,7 @@ public static DateAdd addValue(Object value, String unit) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link DateAdd}.
 		 */
+		@Contract("_ -> new")
 		public DateAdd toDateOf(AggregationExpression expression) {
 			return toDate(expression);
 		}
@@ -2848,6 +2896,7 @@ public DateAdd toDateOf(AggregationExpression expression) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link DateAdd}.
 		 */
+		@Contract("_ -> new")
 		public DateAdd toDateOf(String fieldReference) {
 			return toDate(Fields.field(fieldReference));
 		}
@@ -2858,6 +2907,7 @@ public DateAdd toDateOf(String fieldReference) {
 		 * @param dateExpression anything that evaluates to a valid date. Must not be {@literal null}.
 		 * @return new instance of {@link DateAdd}.
 		 */
+		@Contract("_ -> new")
 		public DateAdd toDate(Object dateExpression) {
 			return new DateAdd(append("startDate", dateExpression));
 		}
@@ -2868,6 +2918,7 @@ public DateAdd toDate(Object dateExpression) {
 		 * @param timezone must not be {@literal null}. Consider {@link Timezone#none()} instead.
 		 * @return new instance of {@link DateAdd}.
 		 */
+		@Contract("_ -> new")
 		public DateAdd withTimezone(Timezone timezone) {
 			return new DateAdd(appendTimezone(argumentMap(), timezone));
 		}
@@ -2935,6 +2986,7 @@ public static DateSubtract subtractValue(Object value, String unit) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link DateSubtract}.
 		 */
+		@Contract("_ -> new")
 		public DateSubtract fromDateOf(AggregationExpression expression) {
 			return fromDate(expression);
 		}
@@ -2945,6 +2997,7 @@ public DateSubtract fromDateOf(AggregationExpression expression) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link DateSubtract}.
 		 */
+		@Contract("_ -> new")
 		public DateSubtract fromDateOf(String fieldReference) {
 			return fromDate(Fields.field(fieldReference));
 		}
@@ -2955,6 +3008,7 @@ public DateSubtract fromDateOf(String fieldReference) {
 		 * @param dateExpression anything that evaluates to a valid date. Must not be {@literal null}.
 		 * @return new instance of {@link DateSubtract}.
 		 */
+		@Contract("_ -> new")
 		public DateSubtract fromDate(Object dateExpression) {
 			return new DateSubtract(append("startDate", dateExpression));
 		}
@@ -2965,6 +3019,7 @@ public DateSubtract fromDate(Object dateExpression) {
 		 * @param timezone must not be {@literal null}. Consider {@link Timezone#none()} instead.
 		 * @return new instance of {@link DateSubtract}.
 		 */
+		@Contract("_ -> new")
 		public DateSubtract withTimezone(Timezone timezone) {
 			return new DateSubtract(appendTimezone(argumentMap(), timezone));
 		}
@@ -3032,6 +3087,7 @@ public static DateDiff diffValue(Object value, String unit) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link DateAdd}.
 		 */
+		@Contract("_ -> new")
 		public DateDiff toDateOf(AggregationExpression expression) {
 			return toDate(expression);
 		}
@@ -3042,6 +3098,7 @@ public DateDiff toDateOf(AggregationExpression expression) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link DateAdd}.
 		 */
+		@Contract("_ -> new")
 		public DateDiff toDateOf(String fieldReference) {
 			return toDate(Fields.field(fieldReference));
 		}
@@ -3052,6 +3109,7 @@ public DateDiff toDateOf(String fieldReference) {
 		 * @param dateExpression anything that evaluates to a valid date. Must not be {@literal null}.
 		 * @return new instance of {@link DateAdd}.
 		 */
+		@Contract("_ -> new")
 		public DateDiff toDate(Object dateExpression) {
 			return new DateDiff(append("startDate", dateExpression));
 		}
@@ -3062,6 +3120,7 @@ public DateDiff toDate(Object dateExpression) {
 		 * @param timezone must not be {@literal null}. Consider {@link Timezone#none()} instead.
 		 * @return new instance of {@link DateAdd}.
 		 */
+		@Contract("_ -> new")
 		public DateDiff withTimezone(Timezone timezone) {
 			return new DateDiff(appendTimezone(argumentMap(), timezone));
 		}
@@ -3073,6 +3132,7 @@ public DateDiff withTimezone(Timezone timezone) {
 		 * @param day must not be {@literal null}.
 		 * @return new instance of {@link DateDiff}.
 		 */
+		@Contract("_ -> new")
 		public DateDiff startOfWeek(Object day) {
 			return new DateDiff(append("startOfWeek", day));
 		}
@@ -3132,6 +3192,7 @@ public static DateTrunc truncateValue(Object value) {
 		 * @param unit must not be {@literal null}.
 		 * @return new instance of {@link DateTrunc}.
 		 */
+		@Contract("_ -> new")
 		public DateTrunc to(String unit) {
 			return new DateTrunc(append("unit", unit));
 		}
@@ -3142,6 +3203,7 @@ public DateTrunc to(String unit) {
 		 * @param unit must not be {@literal null}.
 		 * @return new instance of {@link DateTrunc}.
 		 */
+		@Contract("_ -> new")
 		public DateTrunc to(AggregationExpression unit) {
 			return new DateTrunc(append("unit", unit));
 		}
@@ -3152,6 +3214,7 @@ public DateTrunc to(AggregationExpression unit) {
 		 * @param day must not be {@literal null}.
 		 * @return new instance of {@link DateTrunc}.
 		 */
+		@Contract("_ -> new")
 		public DateTrunc startOfWeek(java.time.DayOfWeek day) {
 			return startOfWeek(day.name().toLowerCase(Locale.US));
 		}
@@ -3162,6 +3225,7 @@ public DateTrunc startOfWeek(java.time.DayOfWeek day) {
 		 * @param day must not be {@literal null}.
 		 * @return new instance of {@link DateTrunc}.
 		 */
+		@Contract("_ -> new")
 		public DateTrunc startOfWeek(String day) {
 			return new DateTrunc(append("startOfWeek", day));
 		}
@@ -3172,6 +3236,7 @@ public DateTrunc startOfWeek(String day) {
 		 * @param binSize must not be {@literal null}.
 		 * @return new instance of {@link DateTrunc}.
 		 */
+		@Contract("_ -> new")
 		public DateTrunc binSize(int binSize) {
 			return binSize((Object) binSize);
 		}
@@ -3182,6 +3247,7 @@ public DateTrunc binSize(int binSize) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link DateTrunc}.
 		 */
+		@Contract("_ -> new")
 		public DateTrunc binSize(AggregationExpression expression) {
 			return binSize((Object) expression);
 		}
@@ -3192,6 +3258,7 @@ public DateTrunc binSize(AggregationExpression expression) {
 		 * @param binSize must not be {@literal null}.
 		 * @return new instance of {@link DateTrunc}.
 		 */
+		@Contract("_ -> new")
 		public DateTrunc binSize(Object binSize) {
 			return new DateTrunc(append("binSize", binSize));
 		}
@@ -3202,6 +3269,7 @@ public DateTrunc binSize(Object binSize) {
 		 * @param timezone must not be {@literal null}. Consider {@link Timezone#none()} instead.
 		 * @return new instance of {@link DateTrunc}.
 		 */
+		@Contract("_ -> new")
 		public DateTrunc withTimezone(Timezone timezone) {
 			return new DateTrunc(appendTimezone(argumentMap(), timezone));
 		}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DensifyOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DensifyOperation.java
index 0da9343ddf..1a559fd26e 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DensifyOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DensifyOperation.java
@@ -25,7 +25,8 @@
 import java.util.stream.Collectors;
 
 import org.bson.Document;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 
@@ -60,6 +61,9 @@ public static DensifyOperationBuilder builder() {
 	@Override
 	public Document toDocument(AggregationOperationContext context) {
 
+		Assert.notNull(field, "Field must be set first");
+		Assert.notNull(range, "Range must be set first");
+
 		Document densify = new Document();
 		densify.put("field", context.getReference(field).getRaw());
 		if (!ObjectUtils.isEmpty(partitionBy)) {
@@ -149,9 +153,9 @@ default Document toDocument() {
 	public static abstract class DensifyRange implements Range {
 
 		private @Nullable DensifyUnit unit;
-		private Number step;
+		private @Nullable Number step;
 
-		public DensifyRange(DensifyUnit unit) {
+		public DensifyRange(@Nullable DensifyUnit unit) {
 			this.unit = unit;
 		}
 
@@ -172,6 +176,7 @@ public Document toDocument(AggregationOperationContext ctx) {
 		 * @param step must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public DensifyRange incrementBy(Number step) {
 			this.step = step;
 			return this;
@@ -183,6 +188,7 @@ public DensifyRange incrementBy(Number step) {
 		 * @param step must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_, _ -> this")
 		public DensifyRange incrementBy(Number step, DensifyUnit unit) {
 			this.step = step;
 			return unit(unit);
@@ -194,6 +200,7 @@ public DensifyRange incrementBy(Number step, DensifyUnit unit) {
 		 * @param unit
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public DensifyRange unit(DensifyUnit unit) {
 
 			this.unit = unit;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentEnhancingOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentEnhancingOperation.java
index 7f260c3785..431215e852 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentEnhancingOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentEnhancingOperation.java
@@ -22,6 +22,7 @@
 import java.util.stream.Collectors;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
 import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation;
 import org.springframework.util.Assert;
@@ -105,7 +106,11 @@ private static Document toSetEntry(Entry<Object, Object> entry, AggregationOpera
 		return new Document(field, value);
 	}
 
-	private static Object computeValue(Object value, AggregationOperationContext context) {
+	private static @Nullable Object computeValue(@Nullable Object value, AggregationOperationContext context) {
+
+		if(value == null) {
+			return value;
+		}
 
 		if (value instanceof Field field) {
 			return context.getReference(field).toString();
@@ -154,7 +159,7 @@ static class ExpressionProjection {
 			this.params = parameters.clone();
 		}
 
-		Object toExpression(AggregationOperationContext context) {
+		@Nullable Object toExpression(AggregationOperationContext context) {
 			return TRANSFORMER.transform(expression, context, params);
 		}
 	}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentOperators.java
index ff63ad834d..a0cd1d056a 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentOperators.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentOperators.java
@@ -18,6 +18,7 @@
 import java.util.Collections;
 
 import org.bson.Document;
+import org.springframework.lang.Contract;
 
 /**
  * Gateway to {@literal document expressions} such as {@literal $rank, $documentNumber, etc.}
@@ -190,6 +191,7 @@ public static Shift shift(AggregationExpression expression) {
 		 * @param shiftBy value to add to the current position.
 		 * @return new instance of {@link Shift}.
 		 */
+		@Contract("_ -> new")
 		public Shift by(int shiftBy) {
 			return new Shift(append("by", shiftBy));
 		}
@@ -200,6 +202,7 @@ public Shift by(int shiftBy) {
 		 * @param value must not be {@literal null}.
 		 * @return new instance of {@link Shift}.
 		 */
+		@Contract("_ -> new")
 		public Shift defaultTo(Object value) {
 			return new Shift(append("default", value));
 		}
@@ -210,6 +213,7 @@ public Shift defaultTo(Object value) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Shift}.
 		 */
+		@Contract("_ -> new")
 		public Shift defaultToValueOf(AggregationExpression expression) {
 			return defaultTo(expression);
 		}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/EvaluationOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/EvaluationOperators.java
index 56f20dde17..dfdc2d620c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/EvaluationOperators.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/EvaluationOperators.java
@@ -16,6 +16,7 @@
 package org.springframework.data.mongodb.core.aggregation;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.query.CriteriaDefinition;
 import org.springframework.util.Assert;
 
@@ -50,8 +51,8 @@ public static EvaluationOperatorFactory valueOf(AggregationExpression expression
 
 	public static class EvaluationOperatorFactory {
 
-		private final String fieldReference;
-		private final AggregationExpression expression;
+		private final @Nullable String fieldReference;
+		private final @Nullable AggregationExpression expression;
 
 		/**
 		 * Creates new {@link EvaluationOperatorFactory} for given {@literal fieldReference}.
@@ -82,6 +83,7 @@ public EvaluationOperatorFactory(AggregationExpression expression) {
 		 *
 		 * @return new instance of {@link Expr}.
 		 */
+		@SuppressWarnings("NullAway")
 		public Expr expr() {
 			return usesFieldRef() ? Expr.valueOf(fieldReference) : Expr.valueOf(expression);
 		}
@@ -91,6 +93,7 @@ public Expr expr() {
 		 *
 		 * @return new instance of {@link Expr}.
 		 */
+		@SuppressWarnings("NullAway")
 		public LastObservationCarriedForward locf() {
 			return usesFieldRef() ? LastObservationCarriedForward.locfValueOf(fieldReference)
 					: LastObservationCarriedForward.locfValueOf(expression);
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java
index 458bc43437..703d5d5f06 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java
@@ -21,8 +21,8 @@
 import java.util.Iterator;
 import java.util.List;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.CompositeIterator;
 import org.springframework.util.ObjectUtils;
@@ -154,8 +154,7 @@ public ExposedFields and(ExposedField field) {
 	 * @param name must not be {@literal null}.
 	 * @return can be {@literal null}.
 	 */
-	@Nullable
-	public ExposedField getField(String name) {
+	public @Nullable ExposedField getField(String name) {
 
 		for (ExposedField field : this) {
 			if (field.canBeReferredToBy(name)) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java
index 131fa8a845..1639a54d48 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java
@@ -17,11 +17,10 @@
 
 import org.bson.Document;
 import org.bson.codecs.configuration.CodecRegistry;
-
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.ExposedFields.DirectFieldReference;
 import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
 import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
 /**
@@ -119,8 +118,7 @@ private FieldReference getReference(@Nullable Field field, String name) {
 	 * @param name must not be {@literal null}.
 	 * @return the resolved reference or {@literal null}.
 	 */
-	@Nullable
-	protected FieldReference resolveExposedField(@Nullable Field field, String name) {
+	protected @Nullable FieldReference resolveExposedField(@Nullable Field field, String name) {
 
 		ExposedField exposedField = exposedFields.getField(name);
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/FacetOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/FacetOperation.java
index f5c73dd09c..d1ca95f659 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/FacetOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/FacetOperation.java
@@ -23,6 +23,7 @@
 import org.bson.Document;
 import org.springframework.data.mongodb.core.aggregation.BucketOperationSupport.Output;
 import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 
 /**
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java
index 83fc7c2b87..7f574a850e 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java
@@ -23,8 +23,9 @@
 import java.util.List;
 import java.util.Map;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.mapping.FieldName;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
@@ -145,14 +146,17 @@ private Fields(Fields existing, Field tail) {
 	 * @param name must not be {@literal null}.
 	 * @return
 	 */
+	@Contract("_ -> new")
 	public Fields and(String name) {
 		return and(new AggregationField(name));
 	}
 
+	@Contract("_ -> new")
 	public Fields and(String name, String target) {
 		return and(new AggregationField(name, target));
 	}
 
+	@Contract("_ -> new")
 	public Fields and(Field field) {
 		return new Fields(this, field);
 	}
@@ -172,8 +176,7 @@ public int size() {
 		return fields.size();
 	}
 
-	@Nullable
-	public Field getField(String name) {
+	public @Nullable Field getField(String name) {
 
 		for (Field field : fields) {
 			if (field.getName().equals(name)) {
@@ -206,7 +209,7 @@ static class AggregationField implements Field {
 
 		private final String raw;
 		private final String name;
-		private final String target;
+		private final @Nullable String target;
 
 		/**
 		 * Creates an aggregation field with the given {@code name}.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperation.java
index f4a5fb4498..bcfc64f2b4 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperation.java
@@ -19,8 +19,9 @@
 import java.util.List;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.query.NearQuery;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 
@@ -80,6 +81,7 @@ private GeoNearOperation(NearQuery nearQuery, String distanceField, @Nullable St
 	 * @return new instance of {@link GeoNearOperation}.
 	 * @since 2.1
 	 */
+	@Contract("_ -> new")
 	public GeoNearOperation useIndex(String key) {
 		return new GeoNearOperation(nearQuery, distanceField, key);
 	}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GraphLookupOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GraphLookupOperation.java
index 72a917c599..ad1f8ae643 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GraphLookupOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GraphLookupOperation.java
@@ -21,10 +21,11 @@
 import java.util.Set;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
 import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation;
 import org.springframework.data.mongodb.core.query.CriteriaDefinition;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 
@@ -35,14 +36,16 @@
  * We recommend to use the static factory method {@link Aggregation#graphLookup(String)} instead of creating instances
  * of this class directly.
  *
- * @see <a href="https://docs.mongodb.org/manual/reference/aggregation/graphLookup/">https://docs.mongodb.org/manual/reference/aggregation/graphLookup/</a>
+ * @see <a href=
+ *      "https://docs.mongodb.org/manual/reference/aggregation/graphLookup/">https://docs.mongodb.org/manual/reference/aggregation/graphLookup/</a>
  * @author Mark Paluch
  * @author Christoph Strobl
  * @since 1.10
  */
 public class GraphLookupOperation implements InheritsFieldsAggregationOperation {
 
-	private static final Set<Class<?>> ALLOWED_START_TYPES = Set.of(AggregationExpression.class, String.class, Field.class, Document.class);
+	private static final Set<Class<?>> ALLOWED_START_TYPES = Set.of(AggregationExpression.class, String.class,
+			Field.class, Document.class);
 
 	private final String from;
 	private final List<Object> startWith;
@@ -126,7 +129,7 @@ public ExposedFields getFields() {
 
 		List<ExposedField> fields = new ArrayList<>(2);
 		fields.add(new ExposedField(as, true));
-		if(depthField != null) {
+		if (depthField != null) {
 			fields.add(new ExposedField(depthField, true));
 		}
 		return ExposedFields.from(fields.toArray(new ExposedField[0]));
@@ -217,10 +220,11 @@ static final class GraphLookupOperationFromBuilder
 			implements FromBuilder, StartWithBuilder, ConnectFromBuilder, ConnectToBuilder {
 
 		private @Nullable String from;
-		private @Nullable List<? extends Object> startWith;
+		private @Nullable List<?> startWith;
 		private @Nullable String connectFrom;
 
 		@Override
+		@Contract("_ -> this")
 		public StartWithBuilder from(String collectionName) {
 
 			Assert.hasText(collectionName, "CollectionName must not be null or empty");
@@ -230,6 +234,7 @@ public StartWithBuilder from(String collectionName) {
 		}
 
 		@Override
+		@Contract("_ -> this")
 		public ConnectFromBuilder startWith(String... fieldReferences) {
 
 			Assert.notNull(fieldReferences, "FieldReferences must not be null");
@@ -246,6 +251,7 @@ public ConnectFromBuilder startWith(String... fieldReferences) {
 		}
 
 		@Override
+		@Contract("_ -> this")
 		public ConnectFromBuilder startWith(AggregationExpression... expressions) {
 
 			Assert.notNull(expressions, "AggregationExpressions must not be null");
@@ -256,6 +262,7 @@ public ConnectFromBuilder startWith(AggregationExpression... expressions) {
 		}
 
 		@Override
+		@Contract("_ -> this")
 		public ConnectFromBuilder startWith(Object... expressions) {
 
 			Assert.notNull(expressions, "Expressions must not be null");
@@ -297,6 +304,7 @@ private void assertStartWithType(Object expression) {
 		}
 
 		@Override
+		@Contract("_ -> this")
 		public ConnectToBuilder connectFrom(String fieldName) {
 
 			Assert.hasText(fieldName, "ConnectFrom must not be null or empty");
@@ -306,10 +314,14 @@ public ConnectToBuilder connectFrom(String fieldName) {
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public GraphLookupOperationBuilder connectTo(String fieldName) {
 
 			Assert.hasText(fieldName, "ConnectTo must not be null or empty");
 
+			Assert.notNull(from, "From must not be null");
+			Assert.notNull(startWith, "startWith must ne set first");
+			Assert.notNull(connectFrom, "ConnectFrom must be set first");
 			return new GraphLookupOperationBuilder(from, startWith, connectFrom, fieldName);
 		}
 	}
@@ -327,8 +339,7 @@ public static final class GraphLookupOperationBuilder {
 		private @Nullable Field depthField;
 		private @Nullable CriteriaDefinition restrictSearchWithMatch;
 
-		private GraphLookupOperationBuilder(String from, List<? extends Object> startWith, String connectFrom,
-				String connectTo) {
+		private GraphLookupOperationBuilder(String from, List<?> startWith, String connectFrom, String connectTo) {
 
 			this.from = from;
 			this.startWith = new ArrayList<>(startWith);
@@ -342,6 +353,7 @@ private GraphLookupOperationBuilder(String from, List<? extends Object> startWit
 		 * @param numberOfRecursions must be greater or equal to zero.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public GraphLookupOperationBuilder maxDepth(long numberOfRecursions) {
 
 			Assert.isTrue(numberOfRecursions >= 0, "Max depth must be >= 0");
@@ -356,6 +368,7 @@ public GraphLookupOperationBuilder maxDepth(long numberOfRecursions) {
 		 * @param fieldName must not be {@literal null} or empty.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public GraphLookupOperationBuilder depthField(String fieldName) {
 
 			Assert.hasText(fieldName, "Depth field name must not be null or empty");
@@ -370,6 +383,7 @@ public GraphLookupOperationBuilder depthField(String fieldName) {
 		 * @param criteriaDefinition must not be {@literal null}.
 		 * @return
 		 */
+		@Contract("_ -> this")
 		public GraphLookupOperationBuilder restrict(CriteriaDefinition criteriaDefinition) {
 
 			Assert.notNull(criteriaDefinition, "CriteriaDefinition must not be null");
@@ -385,6 +399,7 @@ public GraphLookupOperationBuilder restrict(CriteriaDefinition criteriaDefinitio
 		 * @param fieldName must not be {@literal null} or empty.
 		 * @return the final {@link GraphLookupOperation}.
 		 */
+		@Contract("_ -> new")
 		public GraphLookupOperation as(String fieldName) {
 
 			Assert.hasText(fieldName, "As field name must not be null or empty");
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java
index 10d58a7682..b6d36f1baf 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java
@@ -20,10 +20,10 @@
 import java.util.List;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
 import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
 import org.springframework.data.mongodb.core.aggregation.ScriptOperators.Accumulator;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
 /**
@@ -497,6 +497,7 @@ public Operation withAlias(String key) {
 		}
 
 		public ExposedField asField() {
+			Assert.notNull(key, "Key must be set first");
 			return new ExposedField(key, true);
 		}
 
@@ -506,10 +507,12 @@ public Document toDocument(AggregationOperationContext context) {
 			if(op == null && value instanceof Document) {
 				return new Document(key, value);
 			}
+
+			Assert.notNull(op, "Operation keyword must be set");
 			return new Document(key, new Document(op.toString(), value));
 		}
 
-		public Object getValue(AggregationOperationContext context) {
+		public @Nullable Object getValue(AggregationOperationContext context) {
 
 			if (reference == null) {
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/InheritingExposedFieldsAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/InheritingExposedFieldsAggregationOperationContext.java
index ca6a2e2754..739b7c52a9 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/InheritingExposedFieldsAggregationOperationContext.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/InheritingExposedFieldsAggregationOperationContext.java
@@ -16,8 +16,8 @@
 package org.springframework.data.mongodb.core.aggregation;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
-import org.springframework.lang.Nullable;
 
 /**
  * {@link ExposedFieldsAggregationOperationContext} that inherits fields from its parent
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LookupOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LookupOperation.java
index 282ffbd9e0..52cd36b5bb 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LookupOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LookupOperation.java
@@ -18,11 +18,12 @@
 import java.util.function.Supplier;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
 import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation;
 import org.springframework.data.mongodb.core.aggregation.VariableOperators.Let;
 import org.springframework.data.mongodb.core.aggregation.VariableOperators.Let.ExpressionVariable;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 
 /**
@@ -41,17 +42,13 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
 
 	private final String from;
 
-	@Nullable //
-	private final Field localField;
+	private final @Nullable Field localField;
 
-	@Nullable //
-	private final Field foreignField;
+	private final @Nullable Field foreignField;
 
-	@Nullable //
-	private final Let let;
+	private final @Nullable Let let;
 
-	@Nullable //
-	private final AggregationPipeline pipeline;
+	private final @Nullable AggregationPipeline pipeline;
 
 	private final ExposedField as;
 
@@ -281,6 +278,7 @@ public static FromBuilder newBuilder() {
 		}
 
 		@Override
+		@Contract("_ -> this")
 		public LocalFieldBuilder from(String name) {
 
 			Assert.hasText(name, "'From' must not be null or empty");
@@ -289,6 +287,7 @@ public LocalFieldBuilder from(String name) {
 		}
 
 		@Override
+		@Contract("_ -> this")
 		public AsBuilder foreignField(String name) {
 
 			Assert.hasText(name, "'ForeignField' must not be null or empty");
@@ -297,6 +296,7 @@ public AsBuilder foreignField(String name) {
 		}
 
 		@Override
+		@Contract("_ -> this")
 		public ForeignFieldBuilder localField(String name) {
 
 			Assert.hasText(name, "'LocalField' must not be null or empty");
@@ -305,6 +305,7 @@ public ForeignFieldBuilder localField(String name) {
 		}
 
 		@Override
+		@Contract("_ -> this")
 		public PipelineBuilder let(Let let) {
 
 			Assert.notNull(let, "Let must not be null");
@@ -313,6 +314,7 @@ public PipelineBuilder let(Let let) {
 		}
 
 		@Override
+		@Contract("_ -> this")
 		public AsBuilder pipeline(AggregationPipeline pipeline) {
 
 			Assert.notNull(pipeline, "Pipeline must not be null");
@@ -321,9 +323,11 @@ public AsBuilder pipeline(AggregationPipeline pipeline) {
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public LookupOperation as(String name) {
 
 			Assert.hasText(name, "'As' must not be null or empty");
+			Assert.notNull(from, "From must be set first");
 			as = new ExposedField(Fields.field(name), true);
 			return new LookupOperation(from, localField, foreignField, let, pipeline, as);
 		}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MatchOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MatchOperation.java
index da1dbfc027..5f736b55a0 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MatchOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MatchOperation.java
@@ -17,6 +17,7 @@
 
 import org.bson.Document;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.query.CriteriaDefinition;
 import org.springframework.util.Assert;
 
@@ -37,8 +38,8 @@
  */
 public class MatchOperation implements AggregationOperation {
 
-	private final CriteriaDefinition criteriaDefinition;
-	private final AggregationExpression expression;
+	private final @Nullable CriteriaDefinition criteriaDefinition;
+	private final @Nullable AggregationExpression expression;
 
 	/**
 	 * Creates a new {@link MatchOperation} for the given {@link CriteriaDefinition}.
@@ -68,6 +69,7 @@ public MatchOperation(AggregationExpression expression) {
 	}
 
 	@Override
+	@SuppressWarnings("NullAway")
 	public Document toDocument(AggregationOperationContext context) {
 
 		return new Document(getOperator(),
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MergeOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MergeOperation.java
index 314f83fc7c..bda9a3330d 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MergeOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MergeOperation.java
@@ -22,10 +22,11 @@
 import java.util.stream.Collectors;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
 import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation;
 import org.springframework.data.mongodb.core.aggregation.VariableOperators.Let;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
@@ -415,7 +416,7 @@ public Document toDocument(AggregationOperationContext context) {
 	 */
 	public static class MergeOperationBuilder {
 
-		private String collection;
+		private @Nullable String collection;
 		private @Nullable String database;
 		private UniqueMergeId id = UniqueMergeId.id();
 		private @Nullable Let let;
@@ -430,6 +431,7 @@ public MergeOperationBuilder() {}
 		 * @param collection must not be {@literal null} nor empty.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public MergeOperationBuilder intoCollection(String collection) {
 
 			Assert.hasText(collection, "Collection must not be null nor empty");
@@ -444,6 +446,7 @@ public MergeOperationBuilder intoCollection(String collection) {
 		 * @param database must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public MergeOperationBuilder inDatabase(String database) {
 
 			this.database = database;
@@ -456,6 +459,7 @@ public MergeOperationBuilder inDatabase(String database) {
 		 * @param into must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public MergeOperationBuilder into(MergeOperationTarget into) {
 
 			this.database = into.database;
@@ -469,6 +473,7 @@ public MergeOperationBuilder into(MergeOperationTarget into) {
 		 * @param target must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public MergeOperationBuilder target(MergeOperationTarget target) {
 			return into(target);
 		}
@@ -482,6 +487,7 @@ public MergeOperationBuilder target(MergeOperationTarget target) {
 		 * @param fields must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public MergeOperationBuilder on(String... fields) {
 			return id(UniqueMergeId.ofIdFields(fields));
 		}
@@ -493,6 +499,7 @@ public MergeOperationBuilder on(String... fields) {
 		 * @param id must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public MergeOperationBuilder id(UniqueMergeId id) {
 
 			this.id = id;
@@ -506,6 +513,7 @@ public MergeOperationBuilder id(UniqueMergeId id) {
 		 * @param let the variable expressions
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public MergeOperationBuilder let(Let let) {
 
 			this.let = let;
@@ -519,6 +527,7 @@ public MergeOperationBuilder let(Let let) {
 		 * @param let the variable expressions
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public MergeOperationBuilder exposeVariablesOf(Let let) {
 			return let(let);
 		}
@@ -529,6 +538,7 @@ public MergeOperationBuilder exposeVariablesOf(Let let) {
 		 * @param whenMatched must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public MergeOperationBuilder whenMatched(WhenDocumentsMatch whenMatched) {
 
 			this.whenMatched = whenMatched;
@@ -541,6 +551,7 @@ public MergeOperationBuilder whenMatched(WhenDocumentsMatch whenMatched) {
 		 * @param whenMatched must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public MergeOperationBuilder whenDocumentsMatch(WhenDocumentsMatch whenMatched) {
 			return whenMatched(whenMatched);
 		}
@@ -551,6 +562,7 @@ public MergeOperationBuilder whenDocumentsMatch(WhenDocumentsMatch whenMatched)
 		 * @param aggregation must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public MergeOperationBuilder whenDocumentsMatchApply(Aggregation aggregation) {
 			return whenMatched(WhenDocumentsMatch.updateWith(aggregation));
 		}
@@ -561,6 +573,7 @@ public MergeOperationBuilder whenDocumentsMatchApply(Aggregation aggregation) {
 		 * @param whenNotMatched must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public MergeOperationBuilder whenNotMatched(WhenDocumentsDontMatch whenNotMatched) {
 
 			this.whenNotMatched = whenNotMatched;
@@ -573,6 +586,7 @@ public MergeOperationBuilder whenNotMatched(WhenDocumentsDontMatch whenNotMatche
 		 * @param whenNotMatched must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public MergeOperationBuilder whenDocumentsDontMatch(WhenDocumentsDontMatch whenNotMatched) {
 			return whenNotMatched(whenNotMatched);
 		}
@@ -580,7 +594,10 @@ public MergeOperationBuilder whenDocumentsDontMatch(WhenDocumentsDontMatch whenN
 		/**
 		 * @return new instance of {@link MergeOperation}.
 		 */
+		@Contract("-> new")
 		public MergeOperation build() {
+			
+			Assert.notNull(collection, "Collection must not be null");
 			return new MergeOperation(new MergeOperationTarget(database, collection), id, let, whenMatched, whenNotMatched);
 		}
 	}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/NestedDelegatingExpressionAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/NestedDelegatingExpressionAggregationOperationContext.java
index c553a7be02..a5124320f6 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/NestedDelegatingExpressionAggregationOperationContext.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/NestedDelegatingExpressionAggregationOperationContext.java
@@ -20,6 +20,7 @@
 import org.bson.Document;
 
 import org.bson.codecs.configuration.CodecRegistry;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExpressionFieldReference;
 import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
 import org.springframework.util.Assert;
@@ -57,7 +58,7 @@ public Document getMappedObject(Document document) {
 	}
 
 	@Override
-	public Document getMappedObject(Document document, Class<?> type) {
+	public Document getMappedObject(Document document, @Nullable Class<?> type) {
 		return delegate.getMappedObject(document, type);
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java
index 25189241b7..4e21ab7bde 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java
@@ -20,6 +20,7 @@
 import java.util.Collections;
 
 import org.bson.Document;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 
 /**
@@ -277,7 +278,7 @@ public Document toDocument(Object value, AggregationOperationContext context) {
 			return super.toDocument(potentiallyExtractSingleValue(value), context);
 		}
 
-		@SuppressWarnings("unchecked")
+		@SuppressWarnings("NullAway")
 		private Object potentiallyExtractSingleValue(Object value) {
 
 			if (value instanceof Collection<?> collection && collection.size() == 1) {
@@ -385,6 +386,7 @@ public static GetField getField(Field field) {
 		 * @param fieldRef must not be {@literal null}.
 		 * @return new instance of {@link GetField}.
 		 */
+		@Contract("_ -> new")
 		public GetField of(String fieldRef) {
 			return of(Fields.field(fieldRef));
 		}
@@ -396,6 +398,7 @@ public GetField of(String fieldRef) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link GetField}.
 		 */
+		@Contract("_ -> new")
 		public GetField of(AggregationExpression expression) {
 			return of((Object) expression);
 		}
@@ -459,6 +462,7 @@ public static SetField field(Field field) {
 		 * @param fieldRef must not be {@literal null}.
 		 * @return new instance of {@link GetField}.
 		 */
+		@Contract("_ -> new")
 		public SetField input(String fieldRef) {
 			return input(Fields.field(fieldRef));
 		}
@@ -470,6 +474,7 @@ public SetField input(String fieldRef) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link SetField}.
 		 */
+		@Contract("_ -> new")
 		public SetField input(AggregationExpression expression) {
 			return input((Object) expression);
 		}
@@ -481,6 +486,7 @@ public SetField input(AggregationExpression expression) {
 		 * @param fieldRef must not be {@literal null}.
 		 * @return new instance of {@link SetField}.
 		 */
+		@Contract("_ -> new")
 		private SetField input(Object fieldRef) {
 			return new SetField(append("input", fieldRef));
 		}
@@ -491,6 +497,7 @@ private SetField input(Object fieldRef) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link SetField}.
 		 */
+		@Contract("_ -> new")
 		public SetField toValueOf(String fieldReference) {
 			return toValue(Fields.field(fieldReference));
 		}
@@ -502,6 +509,7 @@ public SetField toValueOf(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link SetField}.
 		 */
+		@Contract("_ -> new")
 		public SetField toValueOf(AggregationExpression expression) {
 			return toValue(expression);
 		}
@@ -512,6 +520,7 @@ public SetField toValueOf(AggregationExpression expression) {
 		 * @param value
 		 * @return new instance of {@link SetField}.
 		 */
+		@Contract("_ -> new")
 		public SetField toValue(Object value) {
 			return new SetField(append("value", value));
 		}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/OutOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/OutOperation.java
index 51520f0868..7dbed3a855 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/OutOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/OutOperation.java
@@ -16,8 +16,9 @@
 package org.springframework.data.mongodb.core.aggregation;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.util.BsonUtils;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 
@@ -73,6 +74,7 @@ private OutOperation(@Nullable String databaseName, String collectionName, @Null
 	 * @return new instance of {@link OutOperation}.
 	 * @since 2.2
 	 */
+	@Contract("_ -> new")
 	public OutOperation in(@Nullable String database) {
 		return new OutOperation(database, collectionName, uniqueKey, mode);
 	}
@@ -102,6 +104,7 @@ public OutOperation in(@Nullable String database) {
 	 * @return new instance of {@link OutOperation}.
 	 * @since 2.2
 	 */
+	@Contract("_ -> new")
 	public OutOperation uniqueKey(@Nullable String key) {
 
 		Document uniqueKey = key == null ? null : BsonUtils.toDocumentOrElse(key, it -> new Document(it, 1));
@@ -126,6 +129,7 @@ public OutOperation uniqueKey(@Nullable String key) {
 	 * @return new instance of {@link OutOperation}.
 	 * @since 2.2
 	 */
+	@Contract("_ -> new")
 	public OutOperation uniqueKeyOf(Iterable<String> fields) {
 
 		Assert.notNull(fields, "Fields must not be null");
@@ -144,6 +148,7 @@ public OutOperation uniqueKeyOf(Iterable<String> fields) {
 	 * @return new instance of {@link OutOperation}.
 	 * @since 2.2
 	 */
+	@Contract("_ -> new")
 	public OutOperation mode(OutMode mode) {
 
 		Assert.notNull(mode, "Mode must not be null");
@@ -158,6 +163,7 @@ public OutOperation mode(OutMode mode) {
 	 * @see OutMode#REPLACE_COLLECTION
 	 * @since 2.2
 	 */
+	@Contract("-> new")
 	public OutOperation replaceCollection() {
 		return mode(OutMode.REPLACE_COLLECTION);
 	}
@@ -170,6 +176,7 @@ public OutOperation replaceCollection() {
 	 * @see OutMode#REPLACE
 	 * @since 2.2
 	 */
+	@Contract("-> new")
 	public OutOperation replaceDocuments() {
 		return mode(OutMode.REPLACE);
 	}
@@ -182,6 +189,7 @@ public OutOperation replaceDocuments() {
 	 * @see OutMode#INSERT
 	 * @since 2.2
 	 */
+	@Contract("-> new")
 	public OutOperation insertDocuments() {
 		return mode(OutMode.INSERT);
 	}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/PrefixingDelegatingAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/PrefixingDelegatingAggregationOperationContext.java
index 9524171fed..54ed40b035 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/PrefixingDelegatingAggregationOperationContext.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/PrefixingDelegatingAggregationOperationContext.java
@@ -25,8 +25,8 @@
 
 import org.bson.Document;
 import org.bson.codecs.configuration.CodecRegistry;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
-import org.springframework.lang.Nullable;
 
 /**
  * {@link AggregationOperationContext} implementation prefixing non-command keys on root level with the given prefix.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java
index 35db2214f5..af7cf5bfb2 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java
@@ -23,13 +23,14 @@
 import java.util.stream.Collectors;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.Cond;
 import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.IfNull;
 import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
 import org.springframework.data.mongodb.core.aggregation.Fields.AggregationField;
 import org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder.FieldProjection;
 import org.springframework.data.mongodb.core.aggregation.VariableOperators.Let.ExpressionVariable;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 
 /**
@@ -149,6 +150,7 @@ public ProjectionOperationBuilder and(AggregationExpression expression) {
 	 * @param fieldNames must not be {@literal null}.
 	 * @return
 	 */
+	@Contract("_ -> new")
 	public ProjectionOperation andExclude(String... fieldNames) {
 
 		List<FieldProjection> excludeProjections = FieldProjection.from(Fields.fields(fieldNames), false);
@@ -161,6 +163,7 @@ public ProjectionOperation andExclude(String... fieldNames) {
 	 * @param fieldNames must not be {@literal null}.
 	 * @return
 	 */
+	@Contract("_ -> new")
 	public ProjectionOperation andInclude(String... fieldNames) {
 
 		List<FieldProjection> projections = FieldProjection.from(Fields.fields(fieldNames), true);
@@ -173,6 +176,7 @@ public ProjectionOperation andInclude(String... fieldNames) {
 	 * @param fields must not be {@literal null}.
 	 * @return
 	 */
+	@Contract("_ -> new")
 	public ProjectionOperation andInclude(Fields fields) {
 		return new ProjectionOperation(this.projections, FieldProjection.from(fields, true));
 	}
@@ -185,6 +189,7 @@ public ProjectionOperation andInclude(Fields fields) {
 	 * @return new instance of {@link ProjectionOperation}.
 	 * @since 2.2
 	 */
+	@Contract("_ -> new")
 	public ProjectionOperation asArray(String name) {
 
 		return new ProjectionOperation(Collections.emptyList(),
@@ -402,7 +407,7 @@ public Document toDocument(AggregationOperationContext context) {
 				return new Document(getExposedField().getName(), toMongoExpression(context, expression, params));
 			}
 
-			protected static Object toMongoExpression(AggregationOperationContext context, String expression,
+			protected static @Nullable Object toMongoExpression(AggregationOperationContext context, String expression,
 					Object[] params) {
 				return TRANSFORMER.transform(expression, context, params);
 			}
@@ -1780,6 +1785,7 @@ public ArrayProjectionOperationBuilder(ProjectionOperation target) {
 		 * @param expression
 		 * @return
 		 */
+		@Contract("_ -> this")
 		public ArrayProjectionOperationBuilder and(AggregationExpression expression) {
 
 			Assert.notNull(expression, "AggregationExpression must not be null");
@@ -1794,6 +1800,7 @@ public ArrayProjectionOperationBuilder and(AggregationExpression expression) {
 		 * @param field
 		 * @return
 		 */
+		@Contract("_ -> this")
 		public ArrayProjectionOperationBuilder and(Field field) {
 
 			Assert.notNull(field, "Field must not be null");
@@ -1808,6 +1815,7 @@ public ArrayProjectionOperationBuilder and(Field field) {
 		 * @param value
 		 * @return
 		 */
+		@Contract("_ -> this")
 		public ArrayProjectionOperationBuilder and(Object value) {
 
 			this.projections.add(value);
@@ -1820,6 +1828,7 @@ public ArrayProjectionOperationBuilder and(Object value) {
 		 * @param name The target property name. Must not be {@literal null}.
 		 * @return new instance of {@link ArrayProjectionOperationBuilder}.
 		 */
+		@Contract("_ -> new")
 		public ProjectionOperation as(String name) {
 
 			return new ProjectionOperation(target.projections,
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/RedactOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/RedactOperation.java
index a370016356..5f16fcfc16 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/RedactOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/RedactOperation.java
@@ -16,8 +16,10 @@
 package org.springframework.data.mongodb.core.aggregation;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.Cond.ThenBuilder;
 import org.springframework.data.mongodb.core.query.CriteriaDefinition;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 
 /**
@@ -33,7 +35,8 @@
  * </pre>
  *
  * @author Christoph Strobl
- * @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/redact/">https://docs.mongodb.com/manual/reference/operator/aggregation/redact/</a>
+ * @see <a href=
+ *      "https://docs.mongodb.com/manual/reference/operator/aggregation/redact/">https://docs.mongodb.com/manual/reference/operator/aggregation/redact/</a>
  * @since 3.0
  */
 public class RedactOperation implements AggregationOperation {
@@ -94,9 +97,9 @@ public static RedactOperationBuilder builder() {
 	 */
 	public static class RedactOperationBuilder {
 
-		private Object when;
-		private Object then;
-		private Object otherwise;
+		private @Nullable Object when;
+		private @Nullable Object then;
+		private @Nullable Object otherwise;
 
 		private RedactOperationBuilder() {
 
@@ -108,6 +111,7 @@ private RedactOperationBuilder() {
 		 * @param criteria must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public RedactOperationBuilder when(CriteriaDefinition criteria) {
 
 			this.when = criteria;
@@ -120,6 +124,7 @@ public RedactOperationBuilder when(CriteriaDefinition criteria) {
 		 * @param condition must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public RedactOperationBuilder when(AggregationExpression condition) {
 
 			this.when = condition;
@@ -132,6 +137,7 @@ public RedactOperationBuilder when(AggregationExpression condition) {
 		 * @param condition must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public RedactOperationBuilder when(Document condition) {
 
 			this.when = condition;
@@ -143,6 +149,7 @@ public RedactOperationBuilder when(Document condition) {
 		 *
 		 * @return this.
 		 */
+		@Contract("-> this")
 		public RedactOperationBuilder thenDescend() {
 			return then(DESCEND);
 		}
@@ -152,6 +159,7 @@ public RedactOperationBuilder thenDescend() {
 		 *
 		 * @return this.
 		 */
+		@Contract("-> this")
 		public RedactOperationBuilder thenKeep() {
 			return then(KEEP);
 		}
@@ -161,6 +169,7 @@ public RedactOperationBuilder thenKeep() {
 		 *
 		 * @return this.
 		 */
+		@Contract("-> this")
 		public RedactOperationBuilder thenPrune() {
 			return then(PRUNE);
 		}
@@ -172,6 +181,7 @@ public RedactOperationBuilder thenPrune() {
 		 * @param then must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public RedactOperationBuilder then(Object then) {
 
 			this.then = then;
@@ -183,6 +193,7 @@ public RedactOperationBuilder then(Object then) {
 		 *
 		 * @return this.
 		 */
+		@Contract("-> this")
 		public RedactOperationBuilder otherwiseDescend() {
 			return otherwise(DESCEND);
 		}
@@ -192,6 +203,7 @@ public RedactOperationBuilder otherwiseDescend() {
 		 *
 		 * @return this.
 		 */
+		@Contract("-> this")
 		public RedactOperationBuilder otherwiseKeep() {
 			return otherwise(KEEP);
 		}
@@ -201,6 +213,7 @@ public RedactOperationBuilder otherwiseKeep() {
 		 *
 		 * @return this.
 		 */
+		@Contract("-> this")
 		public RedactOperationBuilder otherwisePrune() {
 			return otherwise(PRUNE);
 		}
@@ -212,6 +225,7 @@ public RedactOperationBuilder otherwisePrune() {
 		 * @param otherwise must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public RedactOperationBuilder otherwise(Object otherwise) {
 			this.otherwise = otherwise;
 			return this;
@@ -220,7 +234,12 @@ public RedactOperationBuilder otherwise(Object otherwise) {
 		/**
 		 * @return new instance of {@link RedactOperation}.
 		 */
+		@Contract("-> new")
 		public RedactOperation build() {
+
+			Assert.notNull(then, "Then must be set first");
+			Assert.notNull(otherwise, "Otherwise must be set first");
+
 			return new RedactOperation(when().then(then).otherwise(otherwise));
 		}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReplaceRootOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReplaceRootOperation.java
index 130182a001..ec306eb6c5 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReplaceRootOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReplaceRootOperation.java
@@ -23,6 +23,7 @@
 import org.bson.Document;
 import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
 import org.springframework.expression.spel.ast.Projection;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 
 /**
@@ -457,6 +458,7 @@ public DocumentContributor(Object value) {
 		}
 
 		@Override
+		@SuppressWarnings("NullAway")
 		public Document toDocument(AggregationOperationContext context) {
 
 			Document document = new Document("$set", value);
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ScriptOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ScriptOperators.java
index 9eab041e88..ed615d9863 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ScriptOperators.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ScriptOperators.java
@@ -22,15 +22,15 @@
 import java.util.List;
 import java.util.Map;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.ScriptOperators.Accumulator.AccumulatorBuilder;
 import org.springframework.data.mongodb.core.aggregation.ScriptOperators.Accumulator.AccumulatorInitBuilder;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.CollectionUtils;
 
 /**
- * Gateway to {@literal $function} and {@literal $accumulator} aggregation operations.
- * <br />
+ * Gateway to {@literal $function} and {@literal $accumulator} aggregation operations. <br />
  * Using {@link ScriptOperators} as part of the {@link Aggregation} requires MongoDB server to have
  * <a href="https://docs.mongodb.com/master/core/server-side-javascript/">server-side JavaScript</a> execution
  * <a href="https://docs.mongodb.com/master/reference/configuration-options/#security.javascriptEnabled">enabled</a>.
@@ -53,8 +53,8 @@ public static Function function(String body) {
 	}
 
 	/**
-	 * Create a custom <a href="https://docs.mongodb.com/master/reference/operator/aggregation/accumulator/">$accumulator operator</a>
-	 * in Javascript.
+	 * Create a custom <a href="https://docs.mongodb.com/master/reference/operator/aggregation/accumulator/">$accumulator
+	 * operator</a> in Javascript.
 	 *
 	 * @return new instance of {@link AccumulatorInitBuilder}.
 	 */
@@ -74,8 +74,7 @@ public static AccumulatorInitBuilder accumulatorBuilder() {
 	 *     lang: "js"
 	 *   }
 	 * }
-	 * </code>
-	 * <br />
+	 * </code> <br />
 	 * {@link Function} cannot be used as part of {@link org.springframework.data.mongodb.core.schema.MongoJsonSchema
 	 * schema} validation query expression. <br />
 	 * <b>NOTE:</b> <a href="https://docs.mongodb.com/master/core/server-side-javascript/">Server-Side JavaScript</a>
@@ -150,10 +149,12 @@ List<Object> getArgs() {
 			return get(Fields.ARGS.toString());
 		}
 
+		@Nullable
 		String getBody() {
 			return get(Fields.BODY.toString());
 		}
 
+		@Nullable
 		String getLang() {
 			return get(Fields.LANG.toString());
 		}
@@ -178,8 +179,7 @@ public String toString() {
 	 * {@link Accumulator} defines a custom aggregation
 	 * <a href="https://docs.mongodb.com/master/reference/operator/aggregation/accumulator/">$accumulator operator</a>,
 	 * one that maintains its state (e.g. totals, maximums, minimums, and related data) as documents progress through the
-	 * pipeline, in JavaScript.
-	 * <br />
+	 * pipeline, in JavaScript. <br />
 	 * <code class="java">
 	 * {
 	 *   $accumulator: {
@@ -192,8 +192,7 @@ public String toString() {
 	 *     lang: "js"
 	 *   }
 	 * }
-	 * </code>
-	 * <br />
+	 * </code> <br />
 	 * {@link Accumulator} can be used as part of {@link GroupOperation $group}, {@link BucketOperation $bucket} and
 	 * {@link BucketAutoOperation $bucketAuto} pipeline stages. <br />
 	 * <b>NOTE:</b> <a href="https://docs.mongodb.com/master/core/server-side-javascript/">Server-Side JavaScript</a>
@@ -240,8 +239,7 @@ public interface AccumulatorInitBuilder {
 
 			/**
 			 * Define the {@code init} {@link Function} for the {@link Accumulator accumulators} initial state. The function
-			 * receives its arguments from the {@link Function#args(Object...) initArgs} array expression.
-			 * <br />
+			 * receives its arguments from the {@link Function#args(Object...) initArgs} array expression. <br />
 			 * <code class="java">
 			 * function(initArg1, initArg2, ...) {
 			 *   ...
@@ -253,13 +251,16 @@ public interface AccumulatorInitBuilder {
 			 * @return this.
 			 */
 			default AccumulatorAccumulateBuilder init(Function function) {
-				return init(function.getBody()).initArgs(function.getArgs());
+
+				Assert.notNull(function.getBody(), "Function body must not be null");
+
+				List<Object> args = function.getArgs();
+				return init(function.getBody()).initArgs(args != null ? args : List.of());
 			}
 
 			/**
 			 * Define the {@code init} function for the {@link Accumulator accumulators} initial state. The function receives
-			 * its arguments from the {@link AccumulatorInitArgsBuilder#initArgs(Object...)} array expression.
-			 * <br />
+			 * its arguments from the {@link AccumulatorInitArgsBuilder#initArgs(Object...)} array expression. <br />
 			 * <code class="java">
 			 * function(initArg1, initArg2, ...) {
 			 *   ...
@@ -307,8 +308,7 @@ public interface AccumulatorAccumulateBuilder {
 			/**
 			 * Set the {@code accumulate} {@link Function} that updates the state for each document. The functions first
 			 * argument is the current {@code state}, additional arguments can be defined via {@link Function#args(Object...)
-			 * accumulateArgs}.
-			 * <br />
+			 * accumulateArgs}. <br />
 			 * <code class="java">
 			 * function(state, accumArg1, accumArg2, ...) {
 			 *   ...
@@ -320,14 +320,17 @@ public interface AccumulatorAccumulateBuilder {
 			 * @return this.
 			 */
 			default AccumulatorMergeBuilder accumulate(Function function) {
-				return accumulate(function.getBody()).accumulateArgs(function.getArgs());
+
+				Assert.notNull(function.getBody(), "Function body must not be null");
+
+				List<Object> args = function.getArgs();
+				return accumulate(function.getBody()).accumulateArgs(args != null ? args : List.of());
 			}
 
 			/**
 			 * Set the {@code accumulate} function that updates the state for each document. The functions first argument is
 			 * the current {@code state}, additional arguments can be defined via
-			 * {@link AccumulatorAccumulateArgsBuilder#accumulateArgs(Object...)}.
-			 * <br />
+			 * {@link AccumulatorAccumulateArgsBuilder#accumulateArgs(Object...)}. <br />
 			 * <code class="java">
 			 * function(state, accumArg1, accumArg2, ...) {
 			 *   ...
@@ -369,8 +372,7 @@ public interface AccumulatorMergeBuilder {
 			/**
 			 * Set the {@code merge} function used to merge two internal states. <br />
 			 * This might be required because the operation is run on a sharded cluster or when the operator exceeds its
-			 * memory limit.
-			 * <br />
+			 * memory limit. <br />
 			 * <code class="java">
 			 * function(state1, state2) {
 			 *   ...
@@ -388,8 +390,7 @@ public interface AccumulatorFinalizeBuilder {
 
 			/**
 			 * Set the {@code finalize} function used to update the result of the accumulation when all documents have been
-			 * processed.
-			 * <br />
+			 * processed. <br />
 			 * <code class="java">
 			 * function(state) {
 			 *   ...
@@ -414,18 +415,17 @@ static class AccumulatorBuilder
 				implements AccumulatorInitBuilder, AccumulatorInitArgsBuilder, AccumulatorAccumulateBuilder,
 				AccumulatorAccumulateArgsBuilder, AccumulatorMergeBuilder, AccumulatorFinalizeBuilder {
 
-			private List<Object> initArgs;
-			private String initFunction;
-			private List<Object> accumulateArgs;
-			private String accumulateFunction;
-			private String mergeFunction;
-			private String finalizeFunction;
+			private @Nullable List<Object> initArgs;
+			private @Nullable String initFunction;
+			private @Nullable List<Object> accumulateArgs;
+			private @Nullable String accumulateFunction;
+			private @Nullable String mergeFunction;
+			private @Nullable String finalizeFunction;
 			private String lang = "js";
 
 			/**
 			 * Define the {@code init} function for the {@link Accumulator accumulators} initial state. The function receives
-			 * its arguments from the {@link #initArgs(Object...)} array expression.
-			 * <br />
+			 * its arguments from the {@link #initArgs(Object...)} array expression. <br />
 			 * <code class="java">
 			 * function(initArg1, initArg2, ...) {
 			 *   ...
@@ -437,6 +437,7 @@ static class AccumulatorBuilder
 			 * @return this.
 			 */
 			@Override
+			@Contract("_ -> this")
 			public AccumulatorBuilder init(String function) {
 
 				this.initFunction = function;
@@ -450,6 +451,7 @@ public AccumulatorBuilder init(String function) {
 			 * @return this.
 			 */
 			@Override
+			@Contract("_ -> this")
 			public AccumulatorBuilder initArgs(List<Object> args) {
 
 				Assert.notNull(args, "Args must not be null");
@@ -460,8 +462,7 @@ public AccumulatorBuilder initArgs(List<Object> args) {
 
 			/**
 			 * Set the {@code accumulate} function that updates the state for each document. The functions first argument is
-			 * the current {@code state}, additional arguments can be defined via {@link #accumulateArgs(Object...)}.
-			 * <br />
+			 * the current {@code state}, additional arguments can be defined via {@link #accumulateArgs(Object...)}. <br />
 			 * <code class="java">
 			 * function(state, accumArg1, accumArg2, ...) {
 			 *   ...
@@ -473,6 +474,7 @@ public AccumulatorBuilder initArgs(List<Object> args) {
 			 * @return this.
 			 */
 			@Override
+			@Contract("_ -> this")
 			public AccumulatorBuilder accumulate(String function) {
 
 				Assert.notNull(function, "Accumulate function must not be null");
@@ -488,6 +490,7 @@ public AccumulatorBuilder accumulate(String function) {
 			 * @return this.
 			 */
 			@Override
+			@Contract("_ -> this")
 			public AccumulatorBuilder accumulateArgs(List<Object> args) {
 
 				Assert.notNull(args, "Args must not be null");
@@ -499,8 +502,7 @@ public AccumulatorBuilder accumulateArgs(List<Object> args) {
 			/**
 			 * Set the {@code merge} function used to merge two internal states. <br />
 			 * This might be required because the operation is run on a sharded cluster or when the operator exceeds its
-			 * memory limit.
-			 * <br />
+			 * memory limit. <br />
 			 * <code class="java">
 			 * function(state1, state2) {
 			 *   ...
@@ -512,6 +514,7 @@ public AccumulatorBuilder accumulateArgs(List<Object> args) {
 			 * @return this.
 			 */
 			@Override
+			@Contract("_ -> this")
 			public AccumulatorBuilder merge(String function) {
 
 				Assert.notNull(function, "Merge function must not be null");
@@ -526,6 +529,7 @@ public AccumulatorBuilder merge(String function) {
 			 * @param lang must not be {@literal null}. Default is {@literal js}.
 			 * @return this.
 			 */
+			@Contract("_ -> this")
 			public AccumulatorBuilder lang(String lang) {
 
 				Assert.hasText(lang, "Lang must not be null nor empty; The default would be 'js'");
@@ -536,8 +540,7 @@ public AccumulatorBuilder lang(String lang) {
 
 			/**
 			 * Set the {@code finalize} function used to update the result of the accumulation when all documents have been
-			 * processed.
-			 * <br />
+			 * processed. <br />
 			 * <code class="java">
 			 * function(state) {
 			 *   ...
@@ -549,6 +552,7 @@ public AccumulatorBuilder lang(String lang) {
 			 * @return new instance of {@link Accumulator}.
 			 */
 			@Override
+			@Contract("_ -> new")
 			public Accumulator finalize(String function) {
 
 				Assert.notNull(function, "Finalize function must not be null");
@@ -562,6 +566,7 @@ public Accumulator finalize(String function) {
 			}
 
 			@Override
+			@Contract("-> new")
 			public Accumulator build() {
 				return new Accumulator(createArgumentMap());
 			}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java
index 9da80c4668..4db0181d39 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java
@@ -19,6 +19,7 @@
 import java.util.Collections;
 
 import org.springframework.data.domain.Sort;
+import org.springframework.lang.Contract;
 
 /**
  * Gateway to {@literal selection operators} such as {@literal $bottom}.
@@ -69,6 +70,7 @@ public static Bottom bottom(int numberOfResults) {
 		 * @param numberOfResults
 		 * @return new instance of {@link Bottom}.
 		 */
+		@Contract("_ -> new")
 		public Bottom limit(int numberOfResults) {
 			return limit((Object) numberOfResults);
 		}
@@ -80,10 +82,12 @@ public Bottom limit(int numberOfResults) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Bottom}.
 		 */
+		@Contract("_ -> new")
 		public Bottom limit(AggregationExpression expression) {
 			return limit((Object) expression);
 		}
 
+		@Contract("_ -> new")
 		private Bottom limit(Object value) {
 			return new Bottom(append("n", value));
 		}
@@ -94,6 +98,7 @@ private Bottom limit(Object value) {
 		 * @param sort must not be {@literal null}.
 		 * @return new instance of {@link Bottom}.
 		 */
+		@Contract("_ -> new")
 		public Bottom sortBy(Sort sort) {
 			return new Bottom(append("sortBy", sort));
 		}
@@ -104,6 +109,7 @@ public Bottom sortBy(Sort sort) {
 		 * @param out must not be {@literal null}.
 		 * @return new instance of {@link Bottom}.
 		 */
+		@Contract("_ -> new")
 		public Bottom output(Fields out) {
 			return new Bottom(append("output", out));
 		}
@@ -115,6 +121,7 @@ public Bottom output(Fields out) {
 		 * @return new instance of {@link Bottom}.
 		 * @see #output(Fields)
 		 */
+		@Contract("_ -> new")
 		public Bottom output(String... fieldNames) {
 			return output(Fields.fields(fieldNames));
 		}
@@ -126,6 +133,7 @@ public Bottom output(String... fieldNames) {
 		 * @return new instance of {@link Bottom}.
 		 * @see #output(Fields)
 		 */
+		@Contract("_ -> new")
 		public Bottom output(AggregationExpression... out) {
 			return new Bottom(append("output", Arrays.asList(out)));
 		}
@@ -172,6 +180,7 @@ public static Top top(int numberOfResults) {
 		 * @param numberOfResults
 		 * @return new instance of {@link Top}.
 		 */
+		@Contract("_ -> new")
 		public Top limit(int numberOfResults) {
 			return limit((Object) numberOfResults);
 		}
@@ -183,6 +192,7 @@ public Top limit(int numberOfResults) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Top}.
 		 */
+		@Contract("_ -> new")
 		public Top limit(AggregationExpression expression) {
 			return limit((Object) expression);
 		}
@@ -197,6 +207,7 @@ private Top limit(Object value) {
 		 * @param sort must not be {@literal null}.
 		 * @return new instance of {@link Top}.
 		 */
+		@Contract("_ -> new")
 		public Top sortBy(Sort sort) {
 			return new Top(append("sortBy", sort));
 		}
@@ -207,6 +218,7 @@ public Top sortBy(Sort sort) {
 		 * @param out must not be {@literal null}.
 		 * @return new instance of {@link Top}.
 		 */
+		@Contract("_ -> new")
 		public Top output(Fields out) {
 			return new Top(append("output", out));
 		}
@@ -218,6 +230,7 @@ public Top output(Fields out) {
 		 * @return new instance of {@link Top}.
 		 * @see #output(Fields)
 		 */
+		@Contract("_ -> new")
 		public Top output(String... fieldNames) {
 			return output(Fields.fields(fieldNames));
 		}
@@ -229,6 +242,7 @@ public Top output(String... fieldNames) {
 		 * @return new instance of {@link Top}.
 		 * @see #output(Fields)
 		 */
+		@Contract("_ -> new")
 		public Top output(AggregationExpression... out) {
 			return new Top(append("output", Arrays.asList(out)));
 		}
@@ -263,6 +277,7 @@ public static First first(int numberOfResults) {
 		 * @param numberOfResults
 		 * @return new instance of {@link First}.
 		 */
+		@Contract("_ -> new")
 		public First limit(int numberOfResults) {
 			return limit((Object) numberOfResults);
 		}
@@ -274,6 +289,7 @@ public First limit(int numberOfResults) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link First}.
 		 */
+		@Contract("_ -> new")
 		public First limit(AggregationExpression expression) {
 			return limit((Object) expression);
 		}
@@ -288,6 +304,7 @@ private First limit(Object value) {
 		 * @param fieldName must not be {@literal null}.
 		 * @return new instance of {@link First}.
 		 */
+		@Contract("_ -> new")
 		public First of(String fieldName) {
 			return input(fieldName);
 		}
@@ -298,6 +315,7 @@ public First of(String fieldName) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link First}.
 		 */
+		@Contract("_ -> new")
 		public First of(AggregationExpression expression) {
 			return input(expression);
 		}
@@ -308,6 +326,7 @@ public First of(AggregationExpression expression) {
 		 * @param fieldName must not be {@literal null}.
 		 * @return new instance of {@link First}.
 		 */
+		@Contract("_ -> new")
 		public First input(String fieldName) {
 			return new First(append("input", Fields.field(fieldName)));
 		}
@@ -318,6 +337,7 @@ public First input(String fieldName) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link First}.
 		 */
+		@Contract("_ -> new")
 		public First input(AggregationExpression expression) {
 			return new First(append("input", expression));
 		}
@@ -357,6 +377,7 @@ public static Last last(int numberOfResults) {
 		 * @param numberOfResults
 		 * @return new instance of {@link Last}.
 		 */
+		@Contract("_ -> new")
 		public Last limit(int numberOfResults) {
 			return limit((Object) numberOfResults);
 		}
@@ -368,6 +389,7 @@ public Last limit(int numberOfResults) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Last}.
 		 */
+		@Contract("_ -> new")
 		public Last limit(AggregationExpression expression) {
 			return limit((Object) expression);
 		}
@@ -382,6 +404,7 @@ private Last limit(Object value) {
 		 * @param fieldName must not be {@literal null}.
 		 * @return new instance of {@link Last}.
 		 */
+		@Contract("_ -> new")
 		public Last of(String fieldName) {
 			return input(fieldName);
 		}
@@ -392,6 +415,7 @@ public Last of(String fieldName) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Last}.
 		 */
+		@Contract("_ -> new")
 		public Last of(AggregationExpression expression) {
 			return input(expression);
 		}
@@ -402,6 +426,7 @@ public Last of(AggregationExpression expression) {
 		 * @param fieldName must not be {@literal null}.
 		 * @return new instance of {@link Last}.
 		 */
+		@Contract("_ -> new")
 		public Last input(String fieldName) {
 			return new Last(append("input", Fields.field(fieldName)));
 		}
@@ -412,6 +437,7 @@ public Last input(String fieldName) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Last}.
 		 */
+		@Contract("_ -> new")
 		public Last input(AggregationExpression expression) {
 			return new Last(append("input", expression));
 		}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperation.java
index 7f5c1c7722..6ef4f1323f 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperation.java
@@ -19,8 +19,9 @@
 import java.util.LinkedHashMap;
 import java.util.Map;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.SetOperation.FieldAppender.ValueAppender;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 
 /**
  * Adds new fields to documents. {@code $set} outputs documents that contain all existing fields from the input
@@ -82,6 +83,7 @@ public static ValueAppender set(String field) {
 	 * @param value the value to assign.
 	 * @return new instance of {@link SetOperation}.
 	 */
+	@Contract("_, _ -> new")
 	public SetOperation set(Object field, Object value) {
 
 		LinkedHashMap<Object, Object> target = new LinkedHashMap<>(getValueMap());
@@ -131,7 +133,7 @@ public ValueAppender set(String field) {
 			return new ValueAppender() {
 
 				@Override
-				public SetOperation toValue(Object value) {
+				public SetOperation toValue(@Nullable Object value) {
 
 					valueMap.put(field, value);
 					return FieldAppender.this.build();
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperators.java
index 094ef7365b..a99c0926f4 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperators.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperators.java
@@ -19,7 +19,9 @@
 import java.util.Collections;
 import java.util.List;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Sum;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 
 /**
@@ -55,8 +57,8 @@ public static SetOperatorFactory arrayAsSet(AggregationExpression expression) {
 	 */
 	public static class SetOperatorFactory {
 
-		private final String fieldReference;
-		private final AggregationExpression expression;
+		private final @Nullable String fieldReference;
+		private final @Nullable AggregationExpression expression;
 
 		/**
 		 * Creates new {@link SetOperatorFactory} for given {@literal fieldReference}.
@@ -104,6 +106,7 @@ public SetEquals isEqualTo(AggregationExpression... expressions) {
 			return createSetEquals().isEqualTo(expressions);
 		}
 
+		@SuppressWarnings("NullAway")
 		private SetEquals createSetEquals() {
 			return usesFieldRef() ? SetEquals.arrayAsSet(fieldReference) : SetEquals.arrayAsSet(expression);
 		}
@@ -130,6 +133,7 @@ public SetIntersection intersects(AggregationExpression... expressions) {
 			return createSetIntersection().intersects(expressions);
 		}
 
+		@SuppressWarnings("NullAway")
 		private SetIntersection createSetIntersection() {
 			return usesFieldRef() ? SetIntersection.arrayAsSet(fieldReference) : SetIntersection.arrayAsSet(expression);
 		}
@@ -156,6 +160,7 @@ public SetUnion union(AggregationExpression... expressions) {
 			return createSetUnion().union(expressions);
 		}
 
+		@SuppressWarnings("NullAway")
 		private SetUnion createSetUnion() {
 			return usesFieldRef() ? SetUnion.arrayAsSet(fieldReference) : SetUnion.arrayAsSet(expression);
 		}
@@ -182,6 +187,7 @@ public SetDifference differenceTo(AggregationExpression expression) {
 			return createSetDifference().differenceTo(expression);
 		}
 
+		@SuppressWarnings("NullAway")
 		private SetDifference createSetDifference() {
 			return usesFieldRef() ? SetDifference.arrayAsSet(fieldReference) : SetDifference.arrayAsSet(expression);
 		}
@@ -208,6 +214,7 @@ public SetIsSubset isSubsetOf(AggregationExpression expression) {
 			return createSetIsSubset().isSubsetOf(expression);
 		}
 
+		@SuppressWarnings("NullAway")
 		private SetIsSubset createSetIsSubset() {
 			return usesFieldRef() ? SetIsSubset.arrayAsSet(fieldReference) : SetIsSubset.arrayAsSet(expression);
 		}
@@ -218,6 +225,7 @@ private SetIsSubset createSetIsSubset() {
 		 *
 		 * @return new instance of {@link AnyElementTrue}.
 		 */
+		@SuppressWarnings("NullAway")
 		public AnyElementTrue anyElementTrue() {
 			return usesFieldRef() ? AnyElementTrue.arrayAsSet(fieldReference) : AnyElementTrue.arrayAsSet(expression);
 		}
@@ -228,6 +236,7 @@ public AnyElementTrue anyElementTrue() {
 		 *
 		 * @return new instance of {@link AllElementsTrue}.
 		 */
+		@SuppressWarnings("NullAway")
 		public AllElementsTrue allElementsTrue() {
 			return usesFieldRef() ? AllElementsTrue.arrayAsSet(fieldReference) : AllElementsTrue.arrayAsSet(expression);
 		}
@@ -283,6 +292,7 @@ public static SetEquals arrayAsSet(AggregationExpression expression) {
 		 * @param arrayReferences must not be {@literal null}.
 		 * @return new instance of {@link SetEquals}.
 		 */
+		@Contract("_ -> new")
 		public SetEquals isEqualTo(String... arrayReferences) {
 
 			Assert.notNull(arrayReferences, "ArrayReferences must not be null");
@@ -295,6 +305,7 @@ public SetEquals isEqualTo(String... arrayReferences) {
 		 * @param expressions must not be {@literal null}.
 		 * @return new instance of {@link SetEquals}.
 		 */
+		@Contract("_ -> new")
 		public SetEquals isEqualTo(AggregationExpression... expressions) {
 
 			Assert.notNull(expressions, "Expressions must not be null");
@@ -307,6 +318,7 @@ public SetEquals isEqualTo(AggregationExpression... expressions) {
 		 * @param array must not be {@literal null}.
 		 * @return new instance of {@link SetEquals}.
 		 */
+		@Contract("_ -> new")
 		public SetEquals isEqualTo(Object[] array) {
 
 			Assert.notNull(array, "Array must not be null");
@@ -360,6 +372,7 @@ public static SetIntersection arrayAsSet(AggregationExpression expression) {
 		 * @param arrayReferences must not be {@literal null}.
 		 * @return new instance of {@link SetIntersection}.
 		 */
+		@Contract("_ -> new")
 		public SetIntersection intersects(String... arrayReferences) {
 
 			Assert.notNull(arrayReferences, "ArrayReferences must not be null");
@@ -372,6 +385,7 @@ public SetIntersection intersects(String... arrayReferences) {
 		 * @param expressions must not be {@literal null}.
 		 * @return new instance of {@link SetIntersection}.
 		 */
+		@Contract("_ -> new")
 		public SetIntersection intersects(AggregationExpression... expressions) {
 
 			Assert.notNull(expressions, "Expressions must not be null");
@@ -425,6 +439,7 @@ public static SetUnion arrayAsSet(AggregationExpression expression) {
 		 * @param arrayReferences must not be {@literal null}.
 		 * @return new instance of {@link SetUnion}.
 		 */
+		@Contract("_ -> new")
 		public SetUnion union(String... arrayReferences) {
 
 			Assert.notNull(arrayReferences, "ArrayReferences must not be null");
@@ -437,6 +452,7 @@ public SetUnion union(String... arrayReferences) {
 		 * @param expressions must not be {@literal null}.
 		 * @return new instance of {@link SetUnion}.
 		 */
+		@Contract("_ -> new")
 		public SetUnion union(AggregationExpression... expressions) {
 
 			Assert.notNull(expressions, "Expressions must not be null");
@@ -490,6 +506,7 @@ public static SetDifference arrayAsSet(AggregationExpression expression) {
 		 * @param arrayReference must not be {@literal null}.
 		 * @return new instance of {@link SetDifference}.
 		 */
+		@Contract("_ -> new")
 		public SetDifference differenceTo(String arrayReference) {
 
 			Assert.notNull(arrayReference, "ArrayReference must not be null");
@@ -502,6 +519,7 @@ public SetDifference differenceTo(String arrayReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link SetDifference}.
 		 */
+		@Contract("_ -> new")
 		public SetDifference differenceTo(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -555,6 +573,7 @@ public static SetIsSubset arrayAsSet(AggregationExpression expression) {
 		 * @param arrayReference must not be {@literal null}.
 		 * @return new instance of {@link SetIsSubset}.
 		 */
+		@Contract("_ -> new")
 		public SetIsSubset isSubsetOf(String arrayReference) {
 
 			Assert.notNull(arrayReference, "ArrayReference must not be null");
@@ -567,6 +586,7 @@ public SetIsSubset isSubsetOf(String arrayReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link SetIsSubset}.
 		 */
+		@Contract("_ -> new")
 		public SetIsSubset isSubsetOf(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -614,6 +634,7 @@ public static AnyElementTrue arrayAsSet(AggregationExpression expression) {
 			return new AnyElementTrue(Collections.singletonList(expression));
 		}
 
+		@Contract("-> this")
 		public AnyElementTrue anyElementTrue() {
 			return this;
 		}
@@ -659,6 +680,7 @@ public static AllElementsTrue arrayAsSet(AggregationExpression expression) {
 			return new AllElementsTrue(Collections.singletonList(expression));
 		}
 
+		@Contract("-> this")
 		public AllElementsTrue allElementsTrue() {
 			return this;
 		}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetWindowFieldsOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetWindowFieldsOperation.java
index 2b8df539e1..e1fec17811 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetWindowFieldsOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetWindowFieldsOperation.java
@@ -22,8 +22,9 @@
 import java.util.concurrent.TimeUnit;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.domain.Sort;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 
 /**
@@ -31,7 +32,8 @@
  *
  * @author Christoph Strobl
  * @since 3.3
- * @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/setWindowFields/">https://docs.mongodb.com/manual/reference/operator/aggregation/setWindowFields/</a>
+ * @see <a href=
+ *      "https://docs.mongodb.com/manual/reference/operator/aggregation/setWindowFields/">https://docs.mongodb.com/manual/reference/operator/aggregation/setWindowFields/</a>
  */
 public class SetWindowFieldsOperation
 		implements AggregationOperation, FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation {
@@ -137,6 +139,7 @@ public WindowOutput(ComputedField outputField) {
 		 * @param field must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public WindowOutput append(ComputedField field) {
 
 			Assert.notNull(field, "Field must not be null");
@@ -152,6 +155,7 @@ public WindowOutput append(ComputedField field) {
 		 * @return new instance of {@link ComputedFieldAppender}.
 		 * @see #append(ComputedField)
 		 */
+		@Contract("_ -> new")
 		public ComputedFieldAppender append(AggregationExpression expression) {
 
 			return new ComputedFieldAppender() {
@@ -249,8 +253,7 @@ public AggregationExpression getWindowOperator() {
 			return windowOperator;
 		}
 
-		@Nullable
-		public Window getWindow() {
+		public @Nullable Window getWindow() {
 			return window;
 		}
 	}
@@ -360,6 +363,7 @@ public static class RangeWindowBuilder {
 		 * @param lower eg. {@literal current} or {@literal unbounded}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public RangeWindowBuilder from(String lower) {
 
 			this.lower = lower;
@@ -372,6 +376,7 @@ public RangeWindowBuilder from(String lower) {
 		 * @param upper eg. {@literal current} or {@literal unbounded}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public RangeWindowBuilder to(String upper) {
 
 			this.upper = upper;
@@ -386,6 +391,7 @@ public RangeWindowBuilder to(String upper) {
 		 * @param lower
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public RangeWindowBuilder from(Number lower) {
 
 			this.lower = lower;
@@ -400,6 +406,7 @@ public RangeWindowBuilder from(Number lower) {
 		 * @param upper
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public RangeWindowBuilder to(Number upper) {
 
 			this.upper = upper;
@@ -411,6 +418,7 @@ public RangeWindowBuilder to(Number upper) {
 		 *
 		 * @return this.
 		 */
+		@Contract("-> this")
 		public RangeWindowBuilder fromCurrent() {
 			return from(CURRENT);
 		}
@@ -420,6 +428,7 @@ public RangeWindowBuilder fromCurrent() {
 		 *
 		 * @return this.
 		 */
+		@Contract("-> this")
 		public RangeWindowBuilder fromUnbounded() {
 			return from(UNBOUNDED);
 		}
@@ -429,6 +438,7 @@ public RangeWindowBuilder fromUnbounded() {
 		 *
 		 * @return this.
 		 */
+		@Contract("-> this")
 		public RangeWindowBuilder toCurrent() {
 			return to(CURRENT);
 		}
@@ -438,6 +448,7 @@ public RangeWindowBuilder toCurrent() {
 		 *
 		 * @return this.
 		 */
+		@Contract("-> this")
 		public RangeWindowBuilder toUnbounded() {
 			return to(UNBOUNDED);
 		}
@@ -448,6 +459,7 @@ public RangeWindowBuilder toUnbounded() {
 		 * @param windowUnit must not be {@literal null}. Can be on of {@link Windows}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public RangeWindowBuilder unit(WindowUnit windowUnit) {
 
 			Assert.notNull(windowUnit, "WindowUnit must not be null");
@@ -460,6 +472,7 @@ public RangeWindowBuilder unit(WindowUnit windowUnit) {
 		 *
 		 * @return new instance of {@link RangeWindow}.
 		 */
+		@Contract("-> new")
 		public RangeWindow build() {
 
 			Assert.notNull(lower, "Lower bound must not be null");
@@ -488,20 +501,24 @@ public static class DocumentWindowBuilder {
 		 * @param lower
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public DocumentWindowBuilder from(Number lower) {
 
 			this.lower = lower;
 			return this;
 		}
 
+		@Contract("-> this")
 		public DocumentWindowBuilder fromCurrent() {
 			return from(CURRENT);
 		}
 
+		@Contract("-> this")
 		public DocumentWindowBuilder fromUnbounded() {
 			return from(UNBOUNDED);
 		}
 
+		@Contract("-> this")
 		public DocumentWindowBuilder to(String upper) {
 
 			this.upper = upper;
@@ -514,6 +531,7 @@ public DocumentWindowBuilder to(String upper) {
 		 * @param lower eg. {@literal current} or {@literal unbounded}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public DocumentWindowBuilder from(String lower) {
 
 			this.lower = lower;
@@ -528,20 +546,24 @@ public DocumentWindowBuilder from(String lower) {
 		 * @param upper
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public DocumentWindowBuilder to(Number upper) {
 
 			this.upper = upper;
 			return this;
 		}
 
+		@Contract("-> this")
 		public DocumentWindowBuilder toCurrent() {
 			return to(CURRENT);
 		}
 
+		@Contract("-> this")
 		public DocumentWindowBuilder toUnbounded() {
 			return to(UNBOUNDED);
 		}
 
+		@Contract("-> new")
 		public DocumentWindow build() {
 
 			Assert.notNull(lower, "Lower bound must not be null");
@@ -689,9 +711,9 @@ public enum WindowUnits implements WindowUnit {
 	 */
 	public static class SetWindowFieldsOperationBuilder {
 
-		private Object partitionBy;
-		private SortOperation sortOperation;
-		private WindowOutput output;
+		private @Nullable Object partitionBy;
+		private @Nullable SortOperation sortOperation;
+		private @Nullable WindowOutput output;
 
 		/**
 		 * Specify the field to group by.
@@ -699,6 +721,7 @@ public static class SetWindowFieldsOperationBuilder {
 		 * @param fieldName must not be {@literal null} or null.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public SetWindowFieldsOperationBuilder partitionByField(String fieldName) {
 
 			Assert.hasText(fieldName, "Field name must not be empty or null");
@@ -711,6 +734,7 @@ public SetWindowFieldsOperationBuilder partitionByField(String fieldName) {
 		 * @param expression must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public SetWindowFieldsOperationBuilder partitionByExpression(AggregationExpression expression) {
 			return partitionBy(expression);
 		}
@@ -721,6 +745,7 @@ public SetWindowFieldsOperationBuilder partitionByExpression(AggregationExpressi
 		 * @param fields must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public SetWindowFieldsOperationBuilder sortBy(String... fields) {
 			return sortBy(Sort.by(fields));
 		}
@@ -731,6 +756,7 @@ public SetWindowFieldsOperationBuilder sortBy(String... fields) {
 		 * @param sort must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public SetWindowFieldsOperationBuilder sortBy(Sort sort) {
 			return sortBy(new SortOperation(sort));
 		}
@@ -741,6 +767,7 @@ public SetWindowFieldsOperationBuilder sortBy(Sort sort) {
 		 * @param sort must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public SetWindowFieldsOperationBuilder sortBy(SortOperation sort) {
 
 			Assert.notNull(sort, "SortOperation must not be null");
@@ -755,6 +782,7 @@ public SetWindowFieldsOperationBuilder sortBy(SortOperation sort) {
 		 * @param output must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public SetWindowFieldsOperationBuilder output(WindowOutput output) {
 
 			Assert.notNull(output, "WindowOutput must not be null");
@@ -769,6 +797,7 @@ public SetWindowFieldsOperationBuilder output(WindowOutput output) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link WindowChoice}.
 		 */
+		@Contract("_ -> new")
 		public WindowChoice output(AggregationExpression expression) {
 
 			return new WindowChoice() {
@@ -837,6 +866,7 @@ public interface WindowChoice extends As {
 		 * @param value must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public SetWindowFieldsOperationBuilder partitionBy(Object value) {
 
 			Assert.notNull(value, "Partition By must not be null");
@@ -850,7 +880,10 @@ public SetWindowFieldsOperationBuilder partitionBy(Object value) {
 		 *
 		 * @return new instance of {@link SetWindowFieldsOperation}.
 		 */
+		@Contract("-> new")
 		public SetWindowFieldsOperation build() {
+
+			Assert.notNull(output, "Output must be set first");
 			return new SetWindowFieldsOperation(partitionBy, sortOperation, output);
 		}
 	}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SortByCountOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SortByCountOperation.java
index ffc0aa0654..e6a9a23d31 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SortByCountOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SortByCountOperation.java
@@ -16,7 +16,7 @@
 package org.springframework.data.mongodb.core.aggregation;
 
 import org.bson.Document;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.Assert;
 
 /**
@@ -67,6 +67,7 @@ public SortByCountOperation(AggregationExpression groupByExpression) {
 	}
 
 	@Override
+	@SuppressWarnings("NullAway")
 	public Document toDocument(AggregationOperationContext context) {
 
 		return new Document(getOperator(), groupByExpression == null ? context.getReference(groupByField).toString()
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformer.java
index 3119e2729c..ade4f5328e 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformer.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformer.java
@@ -20,6 +20,7 @@
 import java.util.List;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.core.GenericTypeResolver;
 import org.springframework.data.mongodb.core.spel.ExpressionNode;
 import org.springframework.data.mongodb.core.spel.ExpressionTransformationContextSupport;
@@ -42,7 +43,6 @@
 import org.springframework.expression.spel.standard.SpelExpression;
 import org.springframework.expression.spel.standard.SpelExpressionParser;
 import org.springframework.expression.spel.support.StandardEvaluationContext;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.NumberUtils;
 import org.springframework.util.ObjectUtils;
@@ -83,7 +83,7 @@ class SpelExpressionTransformer implements AggregationExpressionTransformer {
 	 * @param params must not be {@literal null}
 	 * @return
 	 */
-	public Object transform(String expression, AggregationOperationContext context, Object... params) {
+	public @Nullable Object transform(String expression, AggregationOperationContext context, Object... params) {
 
 		Assert.notNull(expression, "Expression must not be null");
 		Assert.notNull(context, "AggregationOperationContext must not be null");
@@ -96,7 +96,7 @@ public Object transform(String expression, AggregationOperationContext context,
 		return transform(new AggregationExpressionTransformationContext<>(node, null, null, context));
 	}
 
-	public Object transform(AggregationExpressionTransformationContext<ExpressionNode> context) {
+	public @Nullable Object transform(AggregationExpressionTransformationContext<ExpressionNode> context) {
 		return lookupConversionFor(context.getCurrentNode()).convert(context);
 	}
 
@@ -137,7 +137,7 @@ private static abstract class ExpressionNodeConversion<T extends ExpressionNode>
 		 *
 		 * @param transformer must not be {@literal null}.
 		 */
-		@SuppressWarnings("unchecked")
+		@SuppressWarnings({"unchecked", "NullAway"})
 		public ExpressionNodeConversion(AggregationExpressionTransformer transformer) {
 
 			Assert.notNull(transformer, "Transformer must not be null");
@@ -165,7 +165,7 @@ protected boolean supports(ExpressionNode node) {
 		 * @param context must not be {@literal null}.
 		 * @return
 		 */
-		protected Object transform(ExpressionNode node, AggregationExpressionTransformationContext<?> context) {
+		protected @Nullable Object transform(ExpressionNode node, AggregationExpressionTransformationContext<?> context) {
 
 			Assert.notNull(node, "ExpressionNode must not be null");
 			Assert.notNull(context, "AggregationExpressionTransformationContext must not be null");
@@ -183,7 +183,7 @@ protected Object transform(ExpressionNode node, AggregationExpressionTransformat
 		 * @param context must not be {@literal null}.
 		 * @return
 		 */
-		protected Object transform(ExpressionNode node, @Nullable ExpressionNode parent, @Nullable Document operation,
+		protected @Nullable Object transform(ExpressionNode node, @Nullable ExpressionNode parent, @Nullable Document operation,
 				AggregationExpressionTransformationContext<?> context) {
 
 			Assert.notNull(node, "ExpressionNode must not be null");
@@ -194,7 +194,7 @@ protected Object transform(ExpressionNode node, @Nullable ExpressionNode parent,
 		}
 
 		@Override
-		public Object transform(AggregationExpressionTransformationContext<ExpressionNode> context) {
+		public @Nullable Object transform(AggregationExpressionTransformationContext<ExpressionNode> context) {
 			return transformer.transform(context);
 		}
 
@@ -204,7 +204,7 @@ public Object transform(AggregationExpressionTransformationContext<ExpressionNod
 		 * @param context
 		 * @return
 		 */
-		protected abstract Object convert(AggregationExpressionTransformationContext<T> context);
+		protected abstract @Nullable Object convert(AggregationExpressionTransformationContext<T> context);
 	}
 
 	/**
@@ -247,6 +247,7 @@ protected Object convert(AggregationExpressionTransformationContext<OperatorNode
 			return operationObject;
 		}
 
+		@SuppressWarnings("NullAway")
 		private Document createOperationObjectAndAddToPreviousArgumentsIfNecessary(
 				AggregationExpressionTransformationContext<OperatorNode> context, OperatorNode currentNode) {
 
@@ -301,7 +302,7 @@ private static class IndexerNodeConversion extends ExpressionNodeConversion<Expr
 		}
 
 		@Override
-		protected Object convert(AggregationExpressionTransformationContext<ExpressionNode> context) {
+		protected @Nullable Object convert(AggregationExpressionTransformationContext<ExpressionNode> context) {
 			return context.addToPreviousOrReturn(context.getCurrentNode().getValue());
 		}
 
@@ -322,9 +323,8 @@ private static class InlineListNodeConversion extends ExpressionNodeConversion<E
 			super(transformer);
 		}
 
-		@Nullable
 		@Override
-		protected Object convert(AggregationExpressionTransformationContext<ExpressionNode> context) {
+		protected @Nullable Object convert(AggregationExpressionTransformationContext<ExpressionNode> context) {
 
 			ExpressionNode currentNode = context.getCurrentNode();
 
@@ -355,7 +355,7 @@ private static class PropertyOrFieldReferenceNodeConversion extends ExpressionNo
 		}
 
 		@Override
-		protected Object convert(AggregationExpressionTransformationContext<ExpressionNode> context) {
+		protected @Nullable Object convert(AggregationExpressionTransformationContext<ExpressionNode> context) {
 
 			String fieldReference = context.getFieldReference().toString();
 			return context.addToPreviousOrReturn(fieldReference);
@@ -381,14 +381,14 @@ private static class LiteralNodeConversion extends ExpressionNodeConversion<Lite
 
 		@Override
 		@SuppressWarnings("unchecked")
-		protected Object convert(AggregationExpressionTransformationContext<LiteralNode> context) {
+		protected @Nullable Object convert(AggregationExpressionTransformationContext<LiteralNode> context) {
 
 			LiteralNode node = context.getCurrentNode();
 			Object value = node.getValue();
 
 			if (context.hasPreviousOperation()) {
 
-				if (node.isUnaryMinus(context.getParentNode())) {
+				if (node.isUnaryMinus(context.getParentNode()) && value != null) {
 					// unary minus operator
 					return NumberUtils.convertNumberToTargetClass(((Number) value).doubleValue() * -1,
 							(Class<Number>) value.getClass()); // retain type, e.g. int to -int
@@ -419,7 +419,7 @@ private static class MethodReferenceNodeConversion extends ExpressionNodeConvers
 		}
 
 		@Override
-		protected Object convert(AggregationExpressionTransformationContext<MethodReferenceNode> context) {
+		protected @Nullable Object convert(AggregationExpressionTransformationContext<MethodReferenceNode> context) {
 
 			MethodReferenceNode node = context.getCurrentNode();
 			AggregationMethodReference methodReference = node.getMethodReference();
@@ -469,7 +469,7 @@ private static class CompoundExpressionNodeConversion extends ExpressionNodeConv
 		}
 
 		@Override
-		protected Object convert(AggregationExpressionTransformationContext<ExpressionNode> context) {
+		protected @Nullable Object convert(AggregationExpressionTransformationContext<ExpressionNode> context) {
 
 			ExpressionNode currentNode = context.getCurrentNode();
 
@@ -503,7 +503,7 @@ static class NotOperatorNodeConversion extends ExpressionNodeConversion<NotOpera
 		}
 
 		@Override
-		protected Object convert(AggregationExpressionTransformationContext<NotOperatorNode> context) {
+		protected @Nullable Object convert(AggregationExpressionTransformationContext<NotOperatorNode> context) {
 
 			NotOperatorNode node = context.getCurrentNode();
 			List<Object> args = new ArrayList<>();
@@ -537,7 +537,7 @@ static class ValueRetrievingNodeConversion extends ExpressionNodeConversion<Expr
 		}
 
 		@Override
-		protected Object convert(AggregationExpressionTransformationContext<ExpressionNode> context) {
+		protected @Nullable Object convert(AggregationExpressionTransformationContext<ExpressionNode> context) {
 
 			Object value = context.getCurrentNode().getValue();
 			return ObjectUtils.isArray(value) ? Arrays.asList(ObjectUtils.toObjectArray(value)) : value;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/StringOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/StringOperators.java
index 9788497601..0f3447a476 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/StringOperators.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/StringOperators.java
@@ -21,8 +21,10 @@
 import java.util.Map;
 import java.util.regex.Pattern;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.domain.Range;
 import org.springframework.data.mongodb.util.RegexFlags;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 
 /**
@@ -60,8 +62,8 @@ public static StringOperatorFactory valueOf(AggregationExpression fieldReference
 	 */
 	public static class StringOperatorFactory {
 
-		private final String fieldReference;
-		private final AggregationExpression expression;
+		private final @Nullable String fieldReference;
+		private final @Nullable AggregationExpression expression;
 
 		/**
 		 * Creates new {@link StringOperatorFactory} for given {@literal fieldReference}.
@@ -126,6 +128,7 @@ public Concat concat(String value) {
 			return createConcat().concat(value);
 		}
 
+		@SuppressWarnings("NullAway")
 		private Concat createConcat() {
 			return usesFieldRef() ? Concat.valueOf(fieldReference) : Concat.valueOf(expression);
 		}
@@ -153,6 +156,7 @@ public Substr substring(int start, int nrOfChars) {
 			return createSubstr().substring(start, nrOfChars);
 		}
 
+		@SuppressWarnings("NullAway")
 		private Substr createSubstr() {
 			return usesFieldRef() ? Substr.valueOf(fieldReference) : Substr.valueOf(expression);
 		}
@@ -162,6 +166,7 @@ private Substr createSubstr() {
 		 *
 		 * @return new instance of {@link ToLower}.
 		 */
+		@SuppressWarnings("NullAway")
 		public ToLower toLower() {
 			return usesFieldRef() ? ToLower.lowerValueOf(fieldReference) : ToLower.lowerValueOf(expression);
 		}
@@ -171,6 +176,7 @@ public ToLower toLower() {
 		 *
 		 * @return new instance of {@link ToUpper}.
 		 */
+		@SuppressWarnings("NullAway")
 		public ToUpper toUpper() {
 			return usesFieldRef() ? ToUpper.upperValueOf(fieldReference) : ToUpper.upperValueOf(expression);
 		}
@@ -214,6 +220,7 @@ public StrCaseCmp strCaseCmpValueOf(AggregationExpression expression) {
 			return createStrCaseCmp().strcasecmpValueOf(expression);
 		}
 
+		@SuppressWarnings("NullAway")
 		private StrCaseCmp createStrCaseCmp() {
 			return usesFieldRef() ? StrCaseCmp.valueOf(fieldReference) : StrCaseCmp.valueOf(expression);
 		}
@@ -260,6 +267,7 @@ public IndexOfBytes indexOf(AggregationExpression expression) {
 			return createIndexOfBytesSubstringBuilder().indexOf(expression);
 		}
 
+		@SuppressWarnings("NullAway")
 		private IndexOfBytes.SubstringBuilder createIndexOfBytesSubstringBuilder() {
 			return usesFieldRef() ? IndexOfBytes.valueOf(fieldReference) : IndexOfBytes.valueOf(expression);
 		}
@@ -306,6 +314,7 @@ public IndexOfCP indexOfCP(AggregationExpression expression) {
 			return createIndexOfCPSubstringBuilder().indexOf(expression);
 		}
 
+		@SuppressWarnings("NullAway")
 		private IndexOfCP.SubstringBuilder createIndexOfCPSubstringBuilder() {
 			return usesFieldRef() ? IndexOfCP.valueOf(fieldReference) : IndexOfCP.valueOf(expression);
 		}
@@ -343,6 +352,7 @@ public Split split(AggregationExpression expression) {
 			return createSplit().split(expression);
 		}
 
+		@SuppressWarnings("NullAway")
 		private Split createSplit() {
 			return usesFieldRef() ? Split.valueOf(fieldReference) : Split.valueOf(expression);
 		}
@@ -353,6 +363,7 @@ private Split createSplit() {
 		 *
 		 * @return new instance of {@link StrLenBytes}.
 		 */
+		@SuppressWarnings("NullAway")
 		public StrLenBytes length() {
 			return usesFieldRef() ? StrLenBytes.stringLengthOf(fieldReference) : StrLenBytes.stringLengthOf(expression);
 		}
@@ -363,6 +374,7 @@ public StrLenBytes length() {
 		 *
 		 * @return new instance of {@link StrLenCP}.
 		 */
+		@SuppressWarnings("NullAway")
 		public StrLenCP lengthCP() {
 			return usesFieldRef() ? StrLenCP.stringLengthOfCP(fieldReference) : StrLenCP.stringLengthOfCP(expression);
 		}
@@ -390,6 +402,7 @@ public SubstrCP substringCP(int codePointStart, int nrOfCodePoints) {
 			return createSubstrCP().substringCP(codePointStart, nrOfCodePoints);
 		}
 
+		@SuppressWarnings("NullAway")
 		private SubstrCP createSubstrCP() {
 			return usesFieldRef() ? SubstrCP.valueOf(fieldReference) : SubstrCP.valueOf(expression);
 		}
@@ -432,6 +445,7 @@ public Trim trim(AggregationExpression expression) {
 			return trim().charsOf(expression);
 		}
 
+		@SuppressWarnings("NullAway")
 		private Trim createTrim() {
 			return usesFieldRef() ? Trim.valueOf(fieldReference) : Trim.valueOf(expression);
 		}
@@ -474,6 +488,7 @@ public LTrim ltrim(AggregationExpression expression) {
 			return ltrim().charsOf(expression);
 		}
 
+		@SuppressWarnings("NullAway")
 		private LTrim createLTrim() {
 			return usesFieldRef() ? LTrim.valueOf(fieldReference) : LTrim.valueOf(expression);
 		}
@@ -516,6 +531,7 @@ public RTrim rtrim(AggregationExpression expression) {
 			return rtrim().charsOf(expression);
 		}
 
+		@SuppressWarnings("NullAway")
 		private RTrim createRTrim() {
 			return usesFieldRef() ? RTrim.valueOf(fieldReference) : RTrim.valueOf(expression);
 		}
@@ -572,6 +588,7 @@ public RegexFind regexFind(String regex, String options) {
 			return createRegexFind().regex(regex).options(options);
 		}
 
+		@SuppressWarnings("NullAway")
 		private RegexFind createRegexFind() {
 			return usesFieldRef() ? RegexFind.valueOf(fieldReference) : RegexFind.valueOf(expression);
 		}
@@ -628,6 +645,7 @@ public RegexFindAll regexFindAll(String regex, String options) {
 			return createRegexFindAll().regex(regex).options(options);
 		}
 
+		@SuppressWarnings("NullAway")
 		private RegexFindAll createRegexFindAll() {
 			return usesFieldRef() ? RegexFindAll.valueOf(fieldReference) : RegexFindAll.valueOf(expression);
 		}
@@ -683,6 +701,7 @@ public RegexMatch regexMatch(String regex, String options) {
 			return createRegexMatch().regex(regex).options(options);
 		}
 
+		@SuppressWarnings("NullAway")
 		private RegexMatch createRegexMatch() {
 			return usesFieldRef() ? RegexMatch.valueOf(fieldReference) : RegexMatch.valueOf(expression);
 		}
@@ -713,6 +732,7 @@ public ReplaceOne replaceOne(AggregationExpression search, String replacement) {
 			return createReplaceOne().findValueOf(search).replacement(replacement);
 		}
 
+		@SuppressWarnings("NullAway")
 		private ReplaceOne createReplaceOne() {
 			return usesFieldRef() ? ReplaceOne.valueOf(fieldReference) : ReplaceOne.valueOf(expression);
 		}
@@ -743,6 +763,7 @@ public ReplaceAll replaceAll(AggregationExpression search, String replacement) {
 			return createReplaceAll().findValueOf(search).replacement(replacement);
 		}
 
+		@SuppressWarnings("NullAway")
 		private ReplaceAll createReplaceAll() {
 			return usesFieldRef() ? ReplaceAll.valueOf(fieldReference) : ReplaceAll.valueOf(expression);
 		}
@@ -810,6 +831,7 @@ public static Concat stringValue(String value) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link Concat}.
 		 */
+		@Contract("_ -> new")
 		public Concat concatValueOf(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -822,6 +844,7 @@ public Concat concatValueOf(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Concat}.
 		 */
+		@Contract("_ -> new")
 		public Concat concatValueOf(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -834,6 +857,7 @@ public Concat concatValueOf(AggregationExpression expression) {
 		 * @param value must not be {@literal null}.
 		 * @return new instance of {@link Concat}.
 		 */
+		@Contract("_ -> new")
 		public Concat concat(String value) {
 			return new Concat(append(value));
 		}
@@ -883,6 +907,7 @@ public static Substr valueOf(AggregationExpression expression) {
 		 * @param start start index (including)
 		 * @return new instance of {@link Substr}.
 		 */
+		@Contract("_ -> new")
 		public Substr substring(int start) {
 			return substring(start, -1);
 		}
@@ -892,6 +917,7 @@ public Substr substring(int start) {
 		 * @param nrOfChars
 		 * @return new instance of {@link Substr}.
 		 */
+		@Contract("_, _ -> new")
 		public Substr substring(int start, int nrOfChars) {
 			return new Substr(append(Arrays.asList(start, nrOfChars)));
 		}
@@ -1055,16 +1081,19 @@ public static StrCaseCmp stringValue(String value) {
 			return new StrCaseCmp(Collections.singletonList(value));
 		}
 
+		@Contract("_ -> new")
 		public StrCaseCmp strcasecmp(String value) {
 			return new StrCaseCmp(append(value));
 		}
 
+		@Contract("_ -> new")
 		public StrCaseCmp strcasecmpValueOf(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
 			return new StrCaseCmp(append(Fields.field(fieldReference)));
 		}
 
+		@Contract("_ -> new")
 		public StrCaseCmp strcasecmpValueOf(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -1118,6 +1147,7 @@ public static SubstringBuilder valueOf(AggregationExpression expression) {
 		 * @param range must not be {@literal null}.
 		 * @return new instance of {@link IndexOfBytes}.
 		 */
+		@Contract("_ -> new")
 		public IndexOfBytes within(Range<Long> range) {
 			return new IndexOfBytes(append(AggregationUtils.toRangeValues(range)));
 		}
@@ -1208,6 +1238,7 @@ public static SubstringBuilder valueOf(AggregationExpression expression) {
 		 * @param range must not be {@literal null}.
 		 * @return new instance of {@link IndexOfCP}.
 		 */
+		@Contract("_ -> new")
 		public IndexOfCP within(Range<Long> range) {
 			return new IndexOfCP(append(AggregationUtils.toRangeValues(range)));
 		}
@@ -1298,6 +1329,7 @@ public static Split valueOf(AggregationExpression expression) {
 		 * @param delimiter must not be {@literal null}.
 		 * @return new instance of {@link Split}.
 		 */
+		@Contract("_ -> new")
 		public Split split(String delimiter) {
 
 			Assert.notNull(delimiter, "Delimiter must not be null");
@@ -1310,6 +1342,7 @@ public Split split(String delimiter) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link Split}.
 		 */
+		@Contract("_ -> new")
 		public Split split(Field fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -1322,6 +1355,7 @@ public Split split(Field fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Split}.
 		 */
+		@Contract("_ -> new")
 		public Split split(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -1447,10 +1481,12 @@ public static SubstrCP valueOf(AggregationExpression expression) {
 			return new SubstrCP(Collections.singletonList(expression));
 		}
 
+		@Contract("_ -> new")
 		public SubstrCP substringCP(int start) {
 			return substringCP(start, -1);
 		}
 
+		@Contract("_, _ -> new")
 		public SubstrCP substringCP(int start, int nrOfChars) {
 			return new SubstrCP(append(Arrays.asList(start, nrOfChars)));
 		}
@@ -1501,6 +1537,7 @@ public static Trim valueOf(AggregationExpression expression) {
 		 * @param chars must not be {@literal null}.
 		 * @return new instance of {@link Trim}.
 		 */
+		@Contract("_ -> new")
 		public Trim chars(String chars) {
 
 			Assert.notNull(chars, "Chars must not be null");
@@ -1514,6 +1551,7 @@ public Trim chars(String chars) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link Trim}.
 		 */
+		@Contract("_ -> new")
 		public Trim charsOf(String fieldReference) {
 			return new Trim(append("chars", Fields.field(fieldReference)));
 		}
@@ -1525,6 +1563,7 @@ public Trim charsOf(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link Trim}.
 		 */
+		@Contract("_ -> new")
 		public Trim charsOf(AggregationExpression expression) {
 			return new Trim(append("chars", expression));
 		}
@@ -1598,6 +1637,7 @@ public static LTrim valueOf(AggregationExpression expression) {
 		 * @param chars must not be {@literal null}.
 		 * @return new instance of {@link LTrim}.
 		 */
+		@Contract("_ -> new")
 		public LTrim chars(String chars) {
 
 			Assert.notNull(chars, "Chars must not be null");
@@ -1611,6 +1651,7 @@ public LTrim chars(String chars) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link LTrim}.
 		 */
+		@Contract("_ -> new")
 		public LTrim charsOf(String fieldReference) {
 			return new LTrim(append("chars", Fields.field(fieldReference)));
 		}
@@ -1622,6 +1663,7 @@ public LTrim charsOf(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link LTrim}.
 		 */
+		@Contract("_ -> new")
 		public LTrim charsOf(AggregationExpression expression) {
 			return new LTrim(append("chars", expression));
 		}
@@ -1677,6 +1719,7 @@ public static RTrim valueOf(AggregationExpression expression) {
 		 * @param chars must not be {@literal null}.
 		 * @return new instance of {@link RTrim}.
 		 */
+		@Contract("_ -> new")
 		public RTrim chars(String chars) {
 
 			Assert.notNull(chars, "Chars must not be null");
@@ -1689,6 +1732,7 @@ public RTrim chars(String chars) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link RTrim}.
 		 */
+		@Contract("_ -> new")
 		public RTrim charsOf(String fieldReference) {
 			return new RTrim(append("chars", Fields.field(fieldReference)));
 		}
@@ -1699,6 +1743,7 @@ public RTrim charsOf(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link RTrim}.
 		 */
+		@Contract("_ -> new")
 		public RTrim charsOf(AggregationExpression expression) {
 			return new RTrim(append("chars", expression));
 		}
@@ -1757,6 +1802,7 @@ public static RegexFind valueOf(AggregationExpression expression) {
 		 * @param options must not be {@literal null}.
 		 * @return new instance of {@link RegexFind}.
 		 */
+		@Contract("_ -> new")
 		public RegexFind options(String options) {
 
 			Assert.notNull(options, "Options must not be null");
@@ -1771,6 +1817,7 @@ public RegexFind options(String options) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link RegexFind}.
 		 */
+		@Contract("_ -> new")
 		public RegexFind optionsOf(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -1785,6 +1832,7 @@ public RegexFind optionsOf(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link RegexFind}.
 		 */
+		@Contract("_ -> new")
 		public RegexFind optionsOf(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -1798,6 +1846,7 @@ public RegexFind optionsOf(AggregationExpression expression) {
 		 * @param regex must not be {@literal null}.
 		 * @return new instance of {@link RegexFind}.
 		 */
+		@Contract("_ -> new")
 		public RegexFind regex(String regex) {
 
 			Assert.notNull(regex, "Regex must not be null");
@@ -1811,6 +1860,7 @@ public RegexFind regex(String regex) {
 		 * @param pattern must not be {@literal null}.
 		 * @return new instance of {@link RegexFind}.
 		 */
+		@Contract("_ -> new")
 		public RegexFind pattern(Pattern pattern) {
 
 			Assert.notNull(pattern, "Pattern must not be null");
@@ -1827,6 +1877,7 @@ public RegexFind pattern(Pattern pattern) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link RegexFind}.
 		 */
+		@Contract("_ -> new")
 		public RegexFind regexOf(String fieldReference) {
 
 			Assert.notNull(fieldReference, "fieldReference must not be null");
@@ -1840,6 +1891,7 @@ public RegexFind regexOf(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link RegexFind}.
 		 */
+		@Contract("_ -> new")
 		public RegexFind regexOf(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -1899,6 +1951,7 @@ public static RegexFindAll valueOf(AggregationExpression expression) {
 		 * @param options must not be {@literal null}.
 		 * @return new instance of {@link RegexFindAll}.
 		 */
+		@Contract("_ -> new")
 		public RegexFindAll options(String options) {
 
 			Assert.notNull(options, "Options must not be null");
@@ -1913,6 +1966,7 @@ public RegexFindAll options(String options) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link RegexFindAll}.
 		 */
+		@Contract("_ -> new")
 		public RegexFindAll optionsOf(String fieldReference) {
 
 			Assert.notNull(fieldReference, "fieldReference must not be null");
@@ -1927,6 +1981,7 @@ public RegexFindAll optionsOf(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link RegexFindAll}.
 		 */
+		@Contract("_ -> new")
 		public RegexFindAll optionsOf(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -1940,6 +1995,7 @@ public RegexFindAll optionsOf(AggregationExpression expression) {
 		 * @param pattern must not be {@literal null}.
 		 * @return new instance of {@link RegexFindAll}.
 		 */
+		@Contract("_ -> new")
 		public RegexFindAll pattern(Pattern pattern) {
 
 			Assert.notNull(pattern, "Pattern must not be null");
@@ -1956,6 +2012,7 @@ public RegexFindAll pattern(Pattern pattern) {
 		 * @param regex must not be {@literal null}.
 		 * @return new instance of {@link RegexFindAll}.
 		 */
+		@Contract("_ -> new")
 		public RegexFindAll regex(String regex) {
 
 			Assert.notNull(regex, "Regex must not be null");
@@ -1969,6 +2026,7 @@ public RegexFindAll regex(String regex) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link RegexFindAll}.
 		 */
+		@Contract("_ -> new")
 		public RegexFindAll regexOf(String fieldReference) {
 
 			Assert.notNull(fieldReference, "fieldReference must not be null");
@@ -1982,6 +2040,7 @@ public RegexFindAll regexOf(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link RegexFindAll}.
 		 */
+		@Contract("_ -> new")
 		public RegexFindAll regexOf(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -2043,6 +2102,7 @@ public static RegexMatch valueOf(AggregationExpression expression) {
 		 * @param options must not be {@literal null}.
 		 * @return new instance of {@link RegexMatch}.
 		 */
+		@Contract("_ -> new")
 		public RegexMatch options(String options) {
 
 			Assert.notNull(options, "Options must not be null");
@@ -2057,6 +2117,7 @@ public RegexMatch options(String options) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link RegexMatch}.
 		 */
+		@Contract("_ -> new")
 		public RegexMatch optionsOf(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -2071,6 +2132,7 @@ public RegexMatch optionsOf(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link RegexMatch}.
 		 */
+		@Contract("_ -> new")
 		public RegexMatch optionsOf(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -2084,6 +2146,7 @@ public RegexMatch optionsOf(AggregationExpression expression) {
 		 * @param pattern must not be {@literal null}.
 		 * @return new instance of {@link RegexMatch}.
 		 */
+		@Contract("_ -> new")
 		public RegexMatch pattern(Pattern pattern) {
 
 			Assert.notNull(pattern, "Pattern must not be null");
@@ -2100,6 +2163,7 @@ public RegexMatch pattern(Pattern pattern) {
 		 * @param regex must not be {@literal null}.
 		 * @return new instance of {@link RegexMatch}.
 		 */
+		@Contract("_ -> new")
 		public RegexMatch regex(String regex) {
 
 			Assert.notNull(regex, "Regex must not be null");
@@ -2113,6 +2177,7 @@ public RegexMatch regex(String regex) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link RegexMatch}.
 		 */
+		@Contract("_ -> new")
 		public RegexMatch regexOf(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -2126,6 +2191,7 @@ public RegexMatch regexOf(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link RegexMatch}.
 		 */
+		@Contract("_ -> new")
 		public RegexMatch regexOf(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -2201,6 +2267,7 @@ public static ReplaceOne valueOf(AggregationExpression expression) {
 		 * @param replacement must not be {@literal null}.
 		 * @return new instance of {@link ReplaceOne}.
 		 */
+		@Contract("_ -> new")
 		public ReplaceOne replacement(String replacement) {
 
 			Assert.notNull(replacement, "Replacement must not be null");
@@ -2215,6 +2282,7 @@ public ReplaceOne replacement(String replacement) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link ReplaceOne}.
 		 */
+		@Contract("_ -> new")
 		public ReplaceOne replacementOf(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -2229,6 +2297,7 @@ public ReplaceOne replacementOf(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link ReplaceOne}.
 		 */
+		@Contract("_ -> new")
 		public ReplaceOne replacementOf(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -2242,6 +2311,7 @@ public ReplaceOne replacementOf(AggregationExpression expression) {
 		 * @param value must not be {@literal null}.
 		 * @return new instance of {@link ReplaceOne}.
 		 */
+		@Contract("_ -> new")
 		public ReplaceOne find(String value) {
 
 			Assert.notNull(value, "Search string must not be null");
@@ -2255,6 +2325,7 @@ public ReplaceOne find(String value) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link ReplaceOne}.
 		 */
+		@Contract("_ -> new")
 		public ReplaceOne findValueOf(String fieldReference) {
 
 			Assert.notNull(fieldReference, "fieldReference must not be null");
@@ -2269,6 +2340,7 @@ public ReplaceOne findValueOf(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link ReplaceOne}.
 		 */
+		@Contract("_ -> new")
 		public ReplaceOne findValueOf(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -2344,6 +2416,7 @@ public static ReplaceAll valueOf(AggregationExpression expression) {
 		 * @param replacement must not be {@literal null}.
 		 * @return new instance of {@link ReplaceAll}.
 		 */
+		@Contract("_ -> new")
 		public ReplaceAll replacement(String replacement) {
 
 			Assert.notNull(replacement, "Replacement must not be null");
@@ -2358,6 +2431,7 @@ public ReplaceAll replacement(String replacement) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link ReplaceAll}.
 		 */
+		@Contract("_ -> new")
 		public ReplaceAll replacementValueOf(String fieldReference) {
 
 			Assert.notNull(fieldReference, "FieldReference must not be null");
@@ -2372,6 +2446,7 @@ public ReplaceAll replacementValueOf(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link ReplaceAll}.
 		 */
+		@Contract("_ -> new")
 		public ReplaceAll replacementValueOf(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
@@ -2385,6 +2460,7 @@ public ReplaceAll replacementValueOf(AggregationExpression expression) {
 		 * @param value must not be {@literal null}.
 		 * @return new instance of {@link ReplaceAll}.
 		 */
+		@Contract("_ -> new")
 		public ReplaceAll find(String value) {
 
 			Assert.notNull(value, "Search string must not be null");
@@ -2398,6 +2474,7 @@ public ReplaceAll find(String value) {
 		 * @param fieldReference must not be {@literal null}.
 		 * @return new instance of {@link ReplaceAll}.
 		 */
+		@Contract("_ -> new")
 		public ReplaceAll findValueOf(String fieldReference) {
 
 			Assert.notNull(fieldReference, "fieldReference must not be null");
@@ -2411,6 +2488,7 @@ public ReplaceAll findValueOf(String fieldReference) {
 		 * @param expression must not be {@literal null}.
 		 * @return new instance of {@link ReplaceAll}.
 		 */
+		@Contract("_ -> new")
 		public ReplaceAll findValueOf(AggregationExpression expression) {
 
 			Assert.notNull(expression, "Expression must not be null");
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SystemVariable.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SystemVariable.java
index 1fcf87d2a0..cc0296c900 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SystemVariable.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SystemVariable.java
@@ -15,7 +15,7 @@
  */
 package org.springframework.data.mongodb.core.aggregation;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /**
  * Describes the system variables available in MongoDB aggregation framework pipeline expressions.
@@ -116,8 +116,7 @@ public String getTarget() {
 		return toString();
 	}
 
-	@Nullable
-	static String variableNameFrom(@Nullable String fieldRef) {
+	static @Nullable String variableNameFrom(@Nullable String fieldRef) {
 
 		if (fieldRef == null || !fieldRef.startsWith(PREFIX) || fieldRef.length() <= 2) {
 			return null;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java
index f30ebf394b..d2d49abf78 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java
@@ -22,7 +22,7 @@
 
 import org.bson.Document;
 import org.bson.codecs.configuration.CodecRegistry;
-
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mapping.MappingException;
 import org.springframework.data.mapping.PersistentPropertyPath;
 import org.springframework.data.mapping.context.MappingContext;
@@ -33,7 +33,6 @@
 import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
 import org.springframework.data.util.Lazy;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
 /**
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperation.java
index 057ada12d5..c93c1bad9e 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperation.java
@@ -19,7 +19,7 @@
 import java.util.List;
 
 import org.bson.Document;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.Assert;
 
 /**
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnsetOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnsetOperation.java
index ff765c37f7..0bcc192ded 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnsetOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnsetOperation.java
@@ -23,6 +23,7 @@
 
 import org.bson.Document;
 import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.CollectionUtils;
 
@@ -67,6 +68,7 @@ public static UnsetOperation unset(String... fields) {
 	 * @param fields must not be {@literal null}.
 	 * @return new instance of {@link UnsetOperation}.
 	 */
+	@Contract("_ -> new")
 	public UnsetOperation and(String... fields) {
 
 		List<Object> target = new ArrayList<>(this.fields);
@@ -80,6 +82,7 @@ public UnsetOperation and(String... fields) {
 	 * @param fields must not be {@literal null}.
 	 * @return new instance of {@link UnsetOperation}.
 	 */
+	@Contract("_ -> new")
 	public UnsetOperation and(Field... fields) {
 
 		List<Object> target = new ArrayList<>(this.fields);
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnwindOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnwindOperation.java
index d59ae01b12..cc0552cd1c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnwindOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnwindOperation.java
@@ -16,8 +16,9 @@
 package org.springframework.data.mongodb.core.aggregation;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 
 /**
@@ -201,6 +202,8 @@ public static PathBuilder newBuilder() {
 		@Override
 		public UnwindOperation preserveNullAndEmptyArrays() {
 
+			Assert.notNull(field, "Path needs to be set first");
+
 			if (arrayIndex != null) {
 				return new UnwindOperation(field, arrayIndex, true);
 			}
@@ -211,6 +214,8 @@ public UnwindOperation preserveNullAndEmptyArrays() {
 		@Override
 		public UnwindOperation skipNullAndEmptyArrays() {
 
+			Assert.notNull(field, "Path needs to be set first");
+
 			if (arrayIndex != null) {
 				return new UnwindOperation(field, arrayIndex, false);
 			}
@@ -219,6 +224,7 @@ public UnwindOperation skipNullAndEmptyArrays() {
 		}
 
 		@Override
+		@Contract("_ -> this")
 		public EmptyArraysBuilder arrayIndex(String field) {
 
 			Assert.hasText(field, "'ArrayIndex' must not be null or empty");
@@ -227,6 +233,7 @@ public EmptyArraysBuilder arrayIndex(String field) {
 		}
 
 		@Override
+		@Contract("-> this")
 		public EmptyArraysBuilder noArrayIndex() {
 
 			arrayIndex = null;
@@ -234,6 +241,7 @@ public EmptyArraysBuilder noArrayIndex() {
 		}
 
 		@Override
+		@Contract("_ -> this")
 		public UnwindOperationBuilder path(String path) {
 
 			Assert.hasText(path, "'Path' must not be null or empty");
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VariableOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VariableOperators.java
index 8e676c72bc..b5a9ca0f21 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VariableOperators.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VariableOperators.java
@@ -22,8 +22,8 @@
 import java.util.stream.Collectors;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.VariableOperators.Let.ExpressionVariable;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
 /**
@@ -223,8 +223,7 @@ public static class Let implements AggregationExpression {
 
 		private final List<ExpressionVariable> vars;
 
-		@Nullable //
-		private final AggregationExpression expression;
+		private final @Nullable AggregationExpression expression;
 
 		private Let(List<ExpressionVariable> vars, @Nullable AggregationExpression expression) {
 
@@ -333,6 +332,7 @@ private Document getMappedVariable(ExpressionVariable var, AggregationOperationC
 			return new Document(var.variableName, var.expression);
 		}
 
+		@SuppressWarnings("NullAway")
 		private Object getMappedIn(AggregationOperationContext context) {
 			return expression.toDocument(new NestedDelegatingExpressionAggregationOperationContext(context,
 					this.vars.stream().map(var -> Fields.field(var.variableName)).collect(Collectors.toList())));
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperation.java
index bcc5fbd7bc..95f1c5b4d2 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperation.java
@@ -24,13 +24,14 @@
 import org.bson.BinaryVector;
 import org.bson.Document;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.domain.Limit;
 import org.springframework.data.domain.Vector;
 import org.springframework.data.mongodb.core.mapping.MongoVector;
 import org.springframework.data.mongodb.core.query.Criteria;
 import org.springframework.data.mongodb.core.query.CriteriaDefinition;
 import org.springframework.lang.Contract;
-import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 
 /**
@@ -55,12 +56,12 @@ public class VectorSearchOperation implements AggregationOperation {
 	private final @Nullable Integer numCandidates;
 	private final QueryPaths path;
 	private final Vector vector;
-	private final String score;
-	private final Consumer<Criteria> scoreCriteria;
+	private final @Nullable String score;
+	private final @Nullable Consumer<Criteria> scoreCriteria;
 
 	private VectorSearchOperation(SearchType searchType, @Nullable CriteriaDefinition filter, String indexName,
 			Limit limit, @Nullable Integer numCandidates, QueryPaths path, Vector vector, @Nullable String searchScore,
-			Consumer<Criteria> scoreCriteria) {
+			@Nullable Consumer<Criteria> scoreCriteria) {
 
 		this.searchType = searchType;
 		this.filter = filter;
@@ -236,7 +237,10 @@ public Document toDocument(AggregationOperationContext context) {
 		}
 
 		$vectorSearch.append("index", indexName);
-		$vectorSearch.append("limit", limit.max());
+
+		if(limit.isLimited()) { // TODO: exception or pass it on?
+			$vectorSearch.append("limit", limit.max());
+		}
 
 		if (numCandidates != null) {
 			$vectorSearch.append("numCandidates", numCandidates);
@@ -296,9 +300,9 @@ public String getOperator() {
 	 */
 	private static class VectorSearchBuilder implements PathContributor, VectorContributor, LimitContributor {
 
-		String index;
-		QueryPath<String> paths;
-		Vector vector;
+		@Nullable String index;
+		@Nullable QueryPath<String> paths;
+		@Nullable Vector vector;
 
 		PathContributor index(String index) {
 			this.index = index;
@@ -314,6 +318,11 @@ public VectorContributor path(String path) {
 
 		@Override
 		public VectorSearchOperation limit(Limit limit) {
+
+			Assert.notNull(index, "Index must be set first");
+			Assert.notNull(paths, "Path must be set first");
+			Assert.notNull(vector, "Vector must be set first");
+			
 			return new VectorSearchOperation(index, QueryPaths.of(paths), limit, vector);
 		}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/package-info.java
index 0e30b8b855..2769990ca6 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/package-info.java
@@ -3,6 +3,6 @@
  * 
  * @since 1.3
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 package org.springframework.data.mongodb.core.aggregation;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/annotation/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/annotation/package-info.java
index 3e08dc1014..9ada2014a0 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/annotation/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/annotation/package-info.java
@@ -1,6 +1,6 @@
 /**
  * Core Spring Data MongoDB annotations not limited to a special use case (like Query,...).
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 package org.springframework.data.mongodb.core.annotation;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/AbstractMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/AbstractMongoConverter.java
index 7a01677939..9b1c744be2 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/AbstractMongoConverter.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/AbstractMongoConverter.java
@@ -20,6 +20,7 @@
 
 import org.bson.types.Code;
 import org.bson.types.ObjectId;
+import org.jspecify.annotations.Nullable;
 import org.springframework.beans.factory.InitializingBean;
 import org.springframework.core.convert.ConversionService;
 import org.springframework.core.convert.support.DefaultConversionService;
@@ -31,7 +32,6 @@
 import org.springframework.data.mongodb.core.convert.MongoConverters.ObjectIdToBigIntegerConverter;
 import org.springframework.data.mongodb.core.convert.MongoConverters.ObjectIdToStringConverter;
 import org.springframework.data.mongodb.core.convert.MongoConverters.StringToObjectIdConverter;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
 /**
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefProxyHandler.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefProxyHandler.java
index 40afbb8c10..3b4dd99d4e 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefProxyHandler.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefProxyHandler.java
@@ -15,8 +15,8 @@
  */
 package org.springframework.data.mongodb.core.convert;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
-import org.springframework.lang.Nullable;
 
 import com.mongodb.DBRef;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolver.java
index 0235694030..ee1f568494 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolver.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolver.java
@@ -18,10 +18,10 @@
 import java.util.List;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.dao.InvalidDataAccessApiUsageException;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
-import org.springframework.lang.Nullable;
 import org.springframework.util.StringUtils;
 
 import com.mongodb.DBRef;
@@ -60,7 +60,7 @@ Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbref, DbR
 	 * @param id will never be {@literal null}.
 	 * @return new instance of {@link DBRef}.
 	 */
-	default DBRef createDbRef(@Nullable org.springframework.data.mongodb.core.mapping.DBRef annotation,
+	default DBRef createDbRef(org.springframework.data.mongodb.core.mapping.@Nullable DBRef annotation,
 			MongoPersistentEntity<?> entity, Object id) {
 
 		if (annotation != null && StringUtils.hasText(annotation.db())) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolverCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolverCallback.java
index bf6b882375..fd80029118 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolverCallback.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolverCallback.java
@@ -15,6 +15,7 @@
  */
 package org.springframework.data.mongodb.core.convert;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
 
 /**
@@ -31,5 +32,5 @@ public interface DbRefResolverCallback {
 	 * @param property will never be {@literal null}.
 	 * @return
 	 */
-	Object resolve(MongoPersistentProperty property);
+	@Nullable Object resolve(MongoPersistentProperty property);
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefProxyHandler.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefProxyHandler.java
index 22b1ce7981..13c0198aa0 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefProxyHandler.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefProxyHandler.java
@@ -18,13 +18,12 @@
 import java.util.function.Function;
 
 import org.bson.Document;
-
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mapping.PersistentPropertyAccessor;
 import org.springframework.data.mapping.context.MappingContext;
 import org.springframework.data.mapping.model.ValueExpressionEvaluator;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
-import org.springframework.lang.Nullable;
 
 import com.mongodb.DBRef;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java
index de66c3ea94..c72bc4b886 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java
@@ -25,6 +25,7 @@
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.dao.InvalidDataAccessApiUsageException;
 import org.springframework.data.mongodb.MongoDatabaseFactory;
 import org.springframework.data.mongodb.MongoDatabaseUtils;
@@ -32,8 +33,8 @@
 import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentProperty;
 import org.springframework.data.mongodb.core.mapping.FieldName;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
 
 import com.mongodb.DBRef;
@@ -71,7 +72,7 @@ public DefaultDbRefResolver(MongoDatabaseFactory mongoDbFactory) {
 	}
 
 	@Override
-	public Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbref, DbRefResolverCallback callback,
+	public @Nullable Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbref, DbRefResolverCallback callback,
 			DbRefProxyHandler handler) {
 
 		Assert.notNull(property, "Property must not be null");
@@ -86,7 +87,7 @@ public Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbr
 	}
 
 	@Override
-	public Document fetch(DBRef dbRef) {
+	public @Nullable Document fetch(DBRef dbRef) {
 		return getReferenceLoader().fetchOne(
 				DocumentReferenceQuery.forSingleDocument(Filters.eq(FieldName.ID.name(), dbRef.getId())),
 				ReferenceCollection.fromDBRef(dbRef));
@@ -171,7 +172,7 @@ private boolean isLazyDbRef(MongoPersistentProperty property) {
 	private static Stream<Document> documentWithId(Object identifier, Collection<Document> documents) {
 
 		return documents.stream() //
-				.filter(it -> it.get(BasicMongoPersistentProperty.ID_FIELD_NAME).equals(identifier)) //
+				.filter(it -> ObjectUtils.nullSafeEquals(it.get(BasicMongoPersistentProperty.ID_FIELD_NAME), identifier)) //
 				.limit(1);
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolverCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolverCallback.java
index 82e5c9d0eb..376e0dd8cd 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolverCallback.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolverCallback.java
@@ -17,6 +17,7 @@
 
 import org.bson.Document;
 import org.bson.conversions.Bson;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mapping.model.ValueExpressionEvaluator;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
 
@@ -53,7 +54,7 @@ class DefaultDbRefResolverCallback implements DbRefResolverCallback {
 	}
 
 	@Override
-	public Object resolve(MongoPersistentProperty property) {
+	public @Nullable Object resolve(MongoPersistentProperty property) {
 		return resolver.getValueInternal(property, surroundingObject, evaluator, path);
 	}
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultMongoTypeMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultMongoTypeMapper.java
index 2c2b52afd5..f5db41f006 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultMongoTypeMapper.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultMongoTypeMapper.java
@@ -23,6 +23,7 @@
 
 import org.bson.Document;
 import org.bson.conversions.Bson;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.convert.CustomConversions;
 import org.springframework.data.convert.DefaultTypeMapper;
 import org.springframework.data.convert.SimpleTypeInformationMapper;
@@ -32,7 +33,6 @@
 import org.springframework.data.mapping.PersistentEntity;
 import org.springframework.data.mapping.context.MappingContext;
 import org.springframework.data.util.TypeInformation;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ObjectUtils;
 
 import com.mongodb.BasicDBList;
@@ -114,7 +114,7 @@ public DefaultMongoTypeMapper(@Nullable String typeKey, List<? extends TypeInfor
 	}
 
 	private DefaultMongoTypeMapper(@Nullable String typeKey, TypeAliasAccessor<Bson> accessor,
-			MappingContext<? extends PersistentEntity<?, ?>, ?> mappingContext,
+			@Nullable MappingContext<? extends PersistentEntity<?, ?>, ?> mappingContext,
 			List<? extends TypeInformationMapper> mappers) {
 
 		super(accessor, mappingContext, mappers);
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceResolver.java
index a7b3d6f21f..4df7c02f91 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceResolver.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceResolver.java
@@ -20,6 +20,7 @@
 import java.util.Collections;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.dao.support.PersistenceExceptionTranslator;
 import org.springframework.data.mongodb.core.mapping.DBRef;
 import org.springframework.data.mongodb.core.mapping.DocumentReference;
@@ -63,7 +64,8 @@ public DefaultReferenceResolver(ReferenceLoader referenceLoader, PersistenceExce
 	}
 
 	@Override
-	public Object resolveReference(MongoPersistentProperty property, Object source,
+	@SuppressWarnings("NullAway")
+	public @Nullable Object resolveReference(MongoPersistentProperty property, Object source,
 			ReferenceLookupDelegate referenceLookupDelegate, MongoEntityReader entityReader) {
 
 		LookupFunction lookupFunction = (property.isCollectionLike() || property.isMap()) ? collectionLookupFunction
@@ -84,6 +86,7 @@ public Object resolveReference(MongoPersistentProperty property, Object source,
 	 * @see DBRef#lazy()
 	 * @see DocumentReference#lazy()
 	 */
+	@SuppressWarnings("NullAway")
 	protected boolean isLazyReference(MongoPersistentProperty property) {
 
 		if (property.isDocumentReference()) {
@@ -106,6 +109,7 @@ LazyLoadingProxyFactory getProxyFactory() {
 		return proxyFactory;
 	}
 
+	@SuppressWarnings("NullAway")
 	private Object createLazyLoadingProxy(MongoPersistentProperty property, Object source,
 			ReferenceLookupDelegate referenceLookupDelegate, LookupFunction lookupFunction, MongoEntityReader entityReader) {
 		return proxyFactory.createLazyLoadingProxy(property,
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java
index c795add9c8..ff50dd5df3 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java
@@ -21,11 +21,11 @@
 
 import org.bson.Document;
 import org.bson.conversions.Bson;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.mapping.FieldName;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
 import org.springframework.data.mongodb.util.BsonUtils;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
 import com.mongodb.DBObject;
@@ -119,8 +119,7 @@ public void put(MongoPersistentProperty prop, @Nullable Object value) {
 	 * @param property must not be {@literal null}.
 	 * @return can be {@literal null}.
 	 */
-	@Nullable
-	public Object get(MongoPersistentProperty property) {
+	public @Nullable Object get(MongoPersistentProperty property) {
 		return BsonUtils.resolveValue(document, getFieldName(property));
 	}
 
@@ -131,8 +130,7 @@ public Object get(MongoPersistentProperty property) {
 	 * @param entity must not be {@literal null}.
 	 * @return
 	 */
-	@Nullable
-	public Object getRawId(MongoPersistentEntity<?> entity) {
+	public @Nullable Object getRawId(MongoPersistentEntity<?> entity) {
 		return entity.hasIdProperty() ? get(entity.getRequiredIdProperty()) : BsonUtils.get(document, FieldName.ID.name());
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java
index 8429584a6f..e03d215088 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java
@@ -70,6 +70,7 @@ class DocumentPointerFactory {
 		this.cache = new WeakHashMap<>();
 	}
 
+	@SuppressWarnings("NullAway")
 	DocumentPointer<?> computePointer(
 			MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
 			MongoPersistentProperty property, Object value, Class<?> typeHint) {
@@ -87,7 +88,7 @@ DocumentPointer<?> computePointer(
 
 		if (usesDefaultLookup(property)) {
 
-			MongoPersistentProperty idProperty = persistentEntity.getIdProperty();
+			MongoPersistentProperty idProperty = persistentEntity.getRequiredIdProperty();
 			Object idValue = persistentEntity.getIdentifierAccessor(value).getIdentifier();
 
 			if (idProperty.hasExplicitWriteTarget()
@@ -114,6 +115,7 @@ DocumentPointer<?> computePointer(
 				.getDocumentPointer(mappingContext, persistentEntity, propertyAccessor);
 	}
 
+	@SuppressWarnings("NullAway")
 	private boolean usesDefaultLookup(MongoPersistentProperty property) {
 
 		if (property.isDocumentReference()) {
@@ -216,9 +218,16 @@ Object updatePlaceholders(org.bson.Document source, org.bson.Document target,
 					MongoPersistentProperty persistentProperty = persistentEntity.getPersistentProperty(entry.getKey());
 					if (persistentProperty != null && persistentProperty.isEntity()) {
 
-						MongoPersistentEntity<?> nestedEntity = mappingContext.getPersistentEntity(persistentProperty.getType());
-						target.put(entry.getKey(), updatePlaceholders(document, new Document(), mappingContext,
-								nestedEntity, nestedEntity.getPropertyAccessor(propertyAccessor.getProperty(persistentProperty))));
+						MongoPersistentEntity<?> nestedEntity = mappingContext.getRequiredPersistentEntity(persistentProperty.getType());
+						Object propertyValue = propertyAccessor.getProperty(persistentProperty);
+
+						if(propertyValue == null) {
+							target.put(entry.getKey(), propertyValue);
+						} else {
+							PersistentPropertyAccessor<?> nestedAccessor = nestedEntity.getPropertyAccessor(propertyValue);
+							target.put(entry.getKey(), updatePlaceholders(document, new Document(), mappingContext,
+								nestedEntity, nestedAccessor));
+						}
 					} else {
 						target.put(entry.getKey(), updatePlaceholders((Document) entry.getValue(), new Document(), mappingContext,
 								persistentEntity, propertyAccessor));
@@ -236,7 +245,7 @@ Object updatePlaceholders(org.bson.Document source, org.bson.Document target,
 					String fieldName = entry.getKey().equals(FieldName.ID.name()) ? "id" : entry.getKey();
 					if (!fieldName.contains(".")) {
 
-						Object targetValue = propertyAccessor.getProperty(persistentEntity.getPersistentProperty(fieldName));
+						Object targetValue = propertyAccessor.getProperty(persistentEntity.getRequiredPersistentProperty(fieldName));
 						target.put(attribute, targetValue);
 						continue;
 					}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPropertyAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPropertyAccessor.java
index ea5ce01b44..a41e17c0ec 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPropertyAccessor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPropertyAccessor.java
@@ -18,11 +18,11 @@
 import java.util.Map;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.context.expression.MapAccessor;
 import org.springframework.expression.EvaluationContext;
 import org.springframework.expression.PropertyAccessor;
 import org.springframework.expression.TypedValue;
-import org.springframework.lang.Nullable;
 
 /**
  * {@link PropertyAccessor} to allow entity based field access to {@link Document}s.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentReferenceSource.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentReferenceSource.java
index bf21781058..b1e894efe9 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentReferenceSource.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentReferenceSource.java
@@ -15,7 +15,8 @@
  */
 package org.springframework.data.mongodb.core.convert;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
+import org.springframework.lang.Contract;
 
 /**
  * The source object to resolve document references upon. Encapsulates the actual source and the reference specific
@@ -56,8 +57,7 @@ public Object getSelf() {
 	 *
 	 * @return can be {@literal null}.
 	 */
-	@Nullable
-	public Object getTargetSource() {
+	public @Nullable Object getTargetSource() {
 		return targetSource;
 	}
 
@@ -67,8 +67,7 @@ public Object getTargetSource() {
 	 * @param source
 	 * @return
 	 */
-	@Nullable
-	static Object getTargetSource(Object source) {
+	static @Nullable Object getTargetSource(@Nullable Object source) {
 		return source instanceof DocumentReferenceSource referenceSource ? referenceSource.getTargetSource() : source;
 	}
 
@@ -78,7 +77,8 @@ static Object getTargetSource(Object source) {
 	 * @param self
 	 * @return
 	 */
-	static Object getSelf(Object self) {
+	@Contract("null -> null")
+	static @Nullable Object getSelf(@Nullable Object self) {
 		return self instanceof DocumentReferenceSource referenceSource ? referenceSource.getSelf() : self;
 	}
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/GeoConverters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/GeoConverters.java
index 2bca260b79..b595ab688f 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/GeoConverters.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/GeoConverters.java
@@ -25,6 +25,7 @@
 
 import org.bson.Document;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.data.convert.ReadingConverter;
 import org.springframework.data.convert.WritingConverter;
@@ -45,6 +46,7 @@
 import org.springframework.data.mongodb.core.geo.GeoJsonPolygon;
 import org.springframework.data.mongodb.core.geo.Sphere;
 import org.springframework.data.mongodb.core.query.GeoCommand;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.NumberUtils;
 import org.springframework.util.ObjectUtils;
@@ -130,7 +132,8 @@ enum DocumentToPointConverter implements Converter<Document, Point> {
 		INSTANCE;
 
 		@Override
-		public Point convert(Document source) {
+		@Contract("null -> null; !null -> !null")
+		public @Nullable Point convert(@Nullable Document source) {
 
 			if (source == null) {
 				return null;
@@ -157,7 +160,8 @@ enum PointToDocumentConverter implements Converter<Point, Document> {
 		INSTANCE;
 
 		@Override
-		public Document convert(Point source) {
+		@Contract("null -> null; !null -> !null")
+		public @Nullable Document convert(@Nullable Point source) {
 			return source == null ? null : new Document("x", source.getX()).append("y", source.getY());
 		}
 	}
@@ -174,7 +178,8 @@ enum BoxToDocumentConverter implements Converter<Box, Document> {
 		INSTANCE;
 
 		@Override
-		public Document convert(Box source) {
+		@Contract("null -> null; !null -> !null")
+		public @Nullable Document convert(@Nullable Box source) {
 
 			if (source == null) {
 				return null;
@@ -199,7 +204,9 @@ enum DocumentToBoxConverter implements Converter<Document, Box> {
 		INSTANCE;
 
 		@Override
-		public Box convert(Document source) {
+		@Contract("null -> null; !null -> !null")
+		@SuppressWarnings("NullAway")
+		public @Nullable Box convert(@Nullable Document source) {
 
 			if (source == null) {
 				return null;
@@ -223,7 +230,8 @@ enum CircleToDocumentConverter implements Converter<Circle, Document> {
 		INSTANCE;
 
 		@Override
-		public Document convert(Circle source) {
+		@Contract("null -> null; !null -> !null")
+		public @Nullable Document convert(@Nullable Circle source) {
 
 			if (source == null) {
 				return null;
@@ -249,7 +257,8 @@ enum DocumentToCircleConverter implements Converter<Document, Circle> {
 		INSTANCE;
 
 		@Override
-		public Circle convert(Document source) {
+		@Contract("null -> null; !null -> !null")
+		public @Nullable Circle convert(@Nullable Document source) {
 
 			if (source == null) {
 				return null;
@@ -261,7 +270,7 @@ public Circle convert(Document source) {
 			Assert.notNull(center, "Center must not be null");
 			Assert.notNull(radius, "Radius must not be null");
 
-			Distance distance = new Distance(toPrimitiveDoubleValue(radius));
+			Distance distance = Distance.of(toPrimitiveDoubleValue(radius));
 
 			if (source.containsKey("metric")) {
 
@@ -286,7 +295,8 @@ enum SphereToDocumentConverter implements Converter<Sphere, Document> {
 		INSTANCE;
 
 		@Override
-		public Document convert(Sphere source) {
+		@Contract("null -> null; !null -> !null")
+		public @Nullable Document convert(@Nullable Sphere source) {
 
 			if (source == null) {
 				return null;
@@ -312,7 +322,8 @@ enum DocumentToSphereConverter implements Converter<Document, Sphere> {
 		INSTANCE;
 
 		@Override
-		public Sphere convert(Document source) {
+		@Contract("null -> null; !null -> !null")
+		public @Nullable Sphere convert(@Nullable Document source) {
 
 			if (source == null) {
 				return null;
@@ -324,7 +335,7 @@ public Sphere convert(Document source) {
 			Assert.notNull(center, "Center must not be null");
 			Assert.notNull(radius, "Radius must not be null");
 
-			Distance distance = new Distance(toPrimitiveDoubleValue(radius));
+			Distance distance = Distance.of(toPrimitiveDoubleValue(radius));
 
 			if (source.containsKey("metric")) {
 
@@ -349,7 +360,8 @@ enum PolygonToDocumentConverter implements Converter<Polygon, Document> {
 		INSTANCE;
 
 		@Override
-		public Document convert(Polygon source) {
+		@Contract("null -> null; !null -> !null")
+		public @Nullable Document convert(@Nullable Polygon source) {
 
 			if (source == null) {
 				return null;
@@ -381,18 +393,20 @@ enum DocumentToPolygonConverter implements Converter<Document, Polygon> {
 
 		@Override
 		@SuppressWarnings({ "unchecked" })
-		public Polygon convert(Document source) {
+		@Contract("null -> null; !null -> !null")
+		public @Nullable Polygon convert(@Nullable Document source) {
 
 			if (source == null) {
 				return null;
 			}
 
 			List<Document> points = (List<Document>) source.get("points");
-			List<Point> newPoints = new ArrayList<>(points.size());
+			Assert.notNull(points, "Points elements of polygon must not be null");
 
+			List<Point> newPoints = new ArrayList<>(points.size());
 			for (Document element : points) {
 
-				Assert.notNull(element, "Point elements of polygon must not be null");
+				Assert.notNull(element, "Point elements of polygon must not contain null");
 				newPoints.add(DocumentToPointConverter.INSTANCE.convert(element));
 			}
 
@@ -412,7 +426,8 @@ enum GeoCommandToDocumentConverter implements Converter<GeoCommand, Document> {
 
 		@Override
 		@SuppressWarnings("rawtypes")
-		public Document convert(GeoCommand source) {
+		@Contract("null -> null; !null -> !null")
+		public @Nullable Document convert(@Nullable GeoCommand source) {
 
 			if (source == null) {
 				return null;
@@ -463,7 +478,8 @@ enum GeoJsonToDocumentConverter implements Converter<GeoJson<?>, Document> {
 		INSTANCE;
 
 		@Override
-		public Document convert(GeoJson<?> source) {
+		@Contract("null -> null; !null -> !null")
+		public @Nullable Document convert(@Nullable GeoJson<?> source) {
 
 			if (source == null) {
 				return null;
@@ -490,7 +506,7 @@ public Document convert(GeoJson<?> source) {
 
 		private Object convertIfNecessary(Object candidate) {
 
-			if (candidate instanceof GeoJson geoJson) {
+			if (candidate instanceof GeoJson<?> geoJson) {
 				return convertIfNecessary(geoJson.getCoordinates());
 			}
 
@@ -551,7 +567,8 @@ enum DocumentToGeoJsonPointConverter implements Converter<Document, GeoJsonPoint
 
 		@Override
 		@SuppressWarnings("unchecked")
-		public GeoJsonPoint convert(Document source) {
+		@Contract("null -> null; !null -> !null")
+		public @Nullable GeoJsonPoint convert(@Nullable Document source) {
 
 			if (source == null) {
 				return null;
@@ -560,7 +577,10 @@ public GeoJsonPoint convert(Document source) {
 			Assert.isTrue(ObjectUtils.nullSafeEquals(source.get("type"), "Point"),
 					String.format("Cannot convert type '%s' to Point", source.get("type")));
 
-			List<Number> dbl = (List<Number>) source.get("coordinates");
+			if(!(source.get("coordinates") instanceof List<?> sourceCoordinates)) {
+				throw new IllegalArgumentException("Coordinates need to be present");
+			}
+			List<Number> dbl = (List<Number>) sourceCoordinates;
 			return new GeoJsonPoint(toPrimitiveDoubleValue(dbl.get(0)), toPrimitiveDoubleValue(dbl.get(1)));
 		}
 	}
@@ -574,7 +594,8 @@ enum DocumentToGeoJsonPolygonConverter implements Converter<Document, GeoJsonPol
 		INSTANCE;
 
 		@Override
-		public GeoJsonPolygon convert(Document source) {
+		@Contract("null -> null; !null -> !null")
+		public @Nullable GeoJsonPolygon convert(@Nullable Document source) {
 
 			if (source == null) {
 				return null;
@@ -596,7 +617,8 @@ enum DocumentToGeoJsonMultiPolygonConverter implements Converter<Document, GeoJs
 		INSTANCE;
 
 		@Override
-		public GeoJsonMultiPolygon convert(Document source) {
+		@Contract("null -> null; !null -> !null")
+		public @Nullable GeoJsonMultiPolygon convert(@Nullable Document source) {
 
 			if (source == null) {
 				return null;
@@ -606,8 +628,9 @@ public GeoJsonMultiPolygon convert(Document source) {
 					String.format("Cannot convert type '%s' to MultiPolygon", source.get("type")));
 
 			List<?> dbl = (List<?>) source.get("coordinates");
-			List<GeoJsonPolygon> polygones = new ArrayList<>();
+			Assert.notNull(dbl, "Source needs to contain coordinates");
 
+			List<GeoJsonPolygon> polygones = new ArrayList<>(dbl.size());
 			for (Object polygon : dbl) {
 				polygones.add(toGeoJsonPolygon((List<?>) polygon));
 			}
@@ -625,7 +648,8 @@ enum DocumentToGeoJsonLineStringConverter implements Converter<Document, GeoJson
 		INSTANCE;
 
 		@Override
-		public GeoJsonLineString convert(Document source) {
+		@Contract("null -> null; !null -> !null")
+		public @Nullable GeoJsonLineString convert(@Nullable Document source) {
 
 			if (source == null) {
 				return null;
@@ -649,7 +673,8 @@ enum DocumentToGeoJsonMultiPointConverter implements Converter<Document, GeoJson
 		INSTANCE;
 
 		@Override
-		public GeoJsonMultiPoint convert(Document source) {
+		@Contract("null -> null; !null -> !null")
+		public @Nullable GeoJsonMultiPoint convert(@Nullable Document source) {
 
 			if (source == null) {
 				return null;
@@ -673,7 +698,8 @@ enum DocumentToGeoJsonMultiLineStringConverter implements Converter<Document, Ge
 		INSTANCE;
 
 		@Override
-		public GeoJsonMultiLineString convert(Document source) {
+		@Contract("null -> null; !null -> !null")
+		public @Nullable GeoJsonMultiLineString convert(@Nullable Document source) {
 
 			if (source == null) {
 				return null;
@@ -682,10 +708,13 @@ public GeoJsonMultiLineString convert(Document source) {
 			Assert.isTrue(ObjectUtils.nullSafeEquals(source.get("type"), "MultiLineString"),
 					String.format("Cannot convert type '%s' to MultiLineString", source.get("type")));
 
-			List<GeoJsonLineString> lines = new ArrayList<>();
-			List<?> cords = (List<?>) source.get("coordinates");
+			if(!(source.get("coordinates") instanceof List<?> coordinates)) {
+				throw new IllegalArgumentException("coordinates need to be present");
+			}
+			
+			List<GeoJsonLineString> lines = new ArrayList<>(coordinates.size());
 
-			for (Object line : cords) {
+			for (Object line : coordinates) {
 				lines.add(new GeoJsonLineString(toListOfPoint((List<?>) line)));
 			}
 			return new GeoJsonMultiLineString(lines);
@@ -700,9 +729,9 @@ enum DocumentToGeoJsonGeometryCollectionConverter implements Converter<Document,
 
 		INSTANCE;
 
-		@SuppressWarnings("rawtypes")
 		@Override
-		public GeoJsonGeometryCollection convert(Document source) {
+		@Contract("null -> null; !null -> !null")
+		public @Nullable GeoJsonGeometryCollection convert(@Nullable Document source) {
 
 			if (source == null) {
 				return null;
@@ -711,8 +740,12 @@ public GeoJsonGeometryCollection convert(Document source) {
 			Assert.isTrue(ObjectUtils.nullSafeEquals(source.get("type"), "GeometryCollection"),
 					String.format("Cannot convert type '%s' to GeometryCollection", source.get("type")));
 
-			List<GeoJson<?>> geometries = new ArrayList<>();
-			for (Object o : (List) source.get("geometries")) {
+			if(!(source.get("geometries") instanceof List<?> sourceGeometries)) {
+				throw new IllegalArgumentException("Geometries need to be present");
+			}
+			
+			List<GeoJson<?>> geometries = new ArrayList<>(sourceGeometries.size());
+			for (Object o : sourceGeometries) {
 				geometries.add(toGenericGeoJson((Document) o));
 			}
 
@@ -732,7 +765,10 @@ static List<Double> toList(Point point) {
 	 * @since 1.7
 	 */
 	@SuppressWarnings("unchecked")
-	static List<Point> toListOfPoint(List<?> listOfCoordinatePairs) {
+	@Contract("null -> fail")
+	static List<Point> toListOfPoint(@Nullable List<?> listOfCoordinatePairs) {
+
+		Assert.notNull(listOfCoordinatePairs, "ListOfCoordinatePairs must not be null");
 
 		List<Point> points = new ArrayList<>(listOfCoordinatePairs.size());
 
@@ -755,7 +791,10 @@ static List<Point> toListOfPoint(List<?> listOfCoordinatePairs) {
 	 * @return never {@literal null}.
 	 * @since 1.7
 	 */
-	static GeoJsonPolygon toGeoJsonPolygon(List<?> dbList) {
+	@Contract("null -> fail")
+	static GeoJsonPolygon toGeoJsonPolygon(@Nullable List<?> dbList) {
+
+		Assert.notNull(dbList, "DbList must not be null");
 
 		GeoJsonPolygon polygon = new GeoJsonPolygon(toListOfPoint((List<?>) dbList.get(0)));
 		return dbList.size() > 1 ? polygon.withInnerRing(toListOfPoint((List<?>) dbList.get(1))) : polygon;
@@ -794,7 +833,8 @@ private static GeoJson<?> toGenericGeoJson(Document source) {
 		throw new IllegalArgumentException(String.format("No converter found capable of converting GeoJson type %s", type));
 	}
 
-	private static double toPrimitiveDoubleValue(Object value) {
+	@Contract("null -> fail")
+	private static double toPrimitiveDoubleValue(@Nullable Object value) {
 
 		Assert.isInstanceOf(Number.class, value, "Argument must be a Number");
 		return NumberUtils.convertNumberToTargetClass((Number) value, Double.class);
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java
index 77aac55813..6329d74d4f 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java
@@ -15,7 +15,7 @@
  */
 package org.springframework.data.mongodb.core.convert;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 import com.mongodb.DBRef;
 
@@ -53,8 +53,7 @@ public interface LazyLoadingProxy {
 	 * @return can be {@literal null}.
 	 * @since 3.3
 	 */
-	@Nullable
-	default Object getSource() {
+	default @Nullable Object getSource() {
 		return toDBRef();
 	}
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxyFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxyFactory.java
index 76539ea431..eff58e7bd4 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxyFactory.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxyFactory.java
@@ -30,6 +30,8 @@
 import org.aopalliance.intercept.MethodInvocation;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
+import org.jspecify.annotations.NullUnmarked;
+import org.jspecify.annotations.Nullable;
 import org.springframework.aop.framework.ProxyFactory;
 import org.springframework.cglib.core.SpringNamingPolicy;
 import org.springframework.cglib.proxy.Callback;
@@ -43,7 +45,6 @@
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
 import org.springframework.data.util.Lock;
 import org.springframework.data.util.Lock.AcquiredLock;
-import org.springframework.lang.Nullable;
 import org.springframework.objenesis.SpringObjenesis;
 import org.springframework.util.ReflectionUtils;
 
@@ -124,7 +125,7 @@ private ProxyFactory prepareProxyFactory(Class<?> propertyType, Supplier<LazyLoa
 	}
 
 	public Object createLazyLoadingProxy(MongoPersistentProperty property, DbRefResolverCallback callback,
-			Object source) {
+			@Nullable Object source) {
 
 		Class<?> propertyType = property.getType();
 		LazyLoadingInterceptor interceptor = new LazyLoadingInterceptor(property, callback, source, exceptionTranslator);
@@ -160,6 +161,7 @@ private Class<?> getEnhancedTypeFor(Class<?> type) {
 		return enhancer.createClass();
 	}
 
+	@NullUnmarked
 	public static class LazyLoadingInterceptor
 			implements MethodInterceptor, org.springframework.cglib.proxy.MethodInterceptor, Serializable {
 
@@ -180,10 +182,10 @@ public static class LazyLoadingInterceptor
 		private final Lock readLock = Lock.of(rwLock.readLock());
 		private final Lock writeLock = Lock.of(rwLock.writeLock());
 
-		private final MongoPersistentProperty property;
-		private final DbRefResolverCallback callback;
-		private final Object source;
-		private final PersistenceExceptionTranslator exceptionTranslator;
+		private final @Nullable MongoPersistentProperty property;
+		private final @Nullable DbRefResolverCallback callback;
+		private final @Nullable Object source;
+		private final @Nullable PersistenceExceptionTranslator exceptionTranslator;
 		private volatile boolean resolved;
 		private @Nullable Object result;
 
@@ -191,18 +193,17 @@ public static class LazyLoadingInterceptor
 		 * @return a {@link LazyLoadingInterceptor} that just continues with the invocation.
 		 * @since 4.0
 		 */
+		@SuppressWarnings("NullAway")
 		public static LazyLoadingInterceptor none() {
 
 			return new LazyLoadingInterceptor(null, null, null, null) {
-				@Nullable
 				@Override
-				public Object invoke(MethodInvocation invocation) throws Throwable {
+				public @Nullable Object invoke(MethodInvocation invocation) throws Throwable {
 					return intercept(invocation.getThis(), invocation.getMethod(), invocation.getArguments(), null);
 				}
 
-				@Nullable
 				@Override
-				public Object intercept(Object o, Method method, Object[] args, MethodProxy proxy) throws Throwable {
+				public @Nullable Object intercept(Object o, Method method, @Nullable Object @Nullable[] args, @Nullable MethodProxy proxy) throws Throwable {
 
 					ReflectionUtils.makeAccessible(method);
 					return method.invoke(o, args);
@@ -210,8 +211,8 @@ public Object intercept(Object o, Method method, Object[] args, MethodProxy prox
 			};
 		}
 
-		public LazyLoadingInterceptor(MongoPersistentProperty property, DbRefResolverCallback callback, Object source,
-				PersistenceExceptionTranslator exceptionTranslator) {
+		public LazyLoadingInterceptor(@Nullable MongoPersistentProperty property, @Nullable DbRefResolverCallback callback, @Nullable Object source,
+			@Nullable PersistenceExceptionTranslator exceptionTranslator) {
 
 			this.property = property;
 			this.callback = callback;
@@ -219,15 +220,13 @@ public LazyLoadingInterceptor(MongoPersistentProperty property, DbRefResolverCal
 			this.exceptionTranslator = exceptionTranslator;
 		}
 
-		@Nullable
 		@Override
-		public Object invoke(MethodInvocation invocation) throws Throwable {
+		public @Nullable Object invoke(MethodInvocation invocation) throws Throwable {
 			return intercept(invocation.getThis(), invocation.getMethod(), invocation.getArguments(), null);
 		}
 
-		@Nullable
 		@Override
-		public Object intercept(Object o, Method method, Object[] args, MethodProxy proxy) throws Throwable {
+		public @Nullable Object intercept(Object o, Method method, @Nullable Object @Nullable[] args, @Nullable MethodProxy proxy) throws Throwable {
 
 			if (INITIALIZE_METHOD.equals(method)) {
 				return ensureResolved();
@@ -247,7 +246,7 @@ public Object intercept(Object o, Method method, Object[] args, MethodProxy prox
 					return proxyToString(source);
 				}
 
-				if (ReflectionUtils.isEqualsMethod(method)) {
+				if (ReflectionUtils.isEqualsMethod(method) && args != null) {
 					return proxyEquals(o, args[0]);
 				}
 
@@ -347,8 +346,8 @@ private void readObject(ObjectInputStream in) throws IOException {
 			}
 		}
 
-		@Nullable
-		private Object resolve() {
+		@SuppressWarnings("NullAway")
+		private @Nullable Object resolve() {
 
 			try (AcquiredLock l = readLock.lock()) {
 				if (resolved) {
@@ -370,7 +369,7 @@ private Object resolve() {
 				return writeLock.execute(() -> callback.resolve(property));
 			} catch (RuntimeException ex) {
 
-				DataAccessException translatedException = exceptionTranslator.translateExceptionIfPossible(ex);
+				DataAccessException translatedException =  exceptionTranslator != null ? exceptionTranslator.translateExceptionIfPossible(ex) : null;
 
 				if (translatedException instanceof ClientSessionException) {
 					throw new LazyLoadingException("Unable to lazily resolve DBRef; Invalid session state", ex);
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java
index 864cc1c3e3..24c3c2f590 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java
@@ -39,12 +39,13 @@
 import org.bson.conversions.Bson;
 import org.bson.json.JsonReader;
 import org.bson.types.ObjectId;
-
+import org.jspecify.annotations.Nullable;
 import org.springframework.beans.BeansException;
 import org.springframework.beans.factory.BeanClassLoaderAware;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.ApplicationContextAware;
 import org.springframework.core.CollectionFactory;
+import org.springframework.core.ResolvableType;
 import org.springframework.core.convert.ConversionService;
 import org.springframework.core.convert.TypeDescriptor;
 import org.springframework.core.convert.support.DefaultConversionService;
@@ -53,6 +54,7 @@
 import org.springframework.core.env.StandardEnvironment;
 import org.springframework.data.annotation.Reference;
 import org.springframework.data.convert.CustomConversions;
+import org.springframework.data.convert.PropertyValueConversions;
 import org.springframework.data.convert.PropertyValueConverter;
 import org.springframework.data.convert.TypeMapper;
 import org.springframework.data.convert.ValueConversionContext;
@@ -71,7 +73,6 @@
 import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider;
 import org.springframework.data.mapping.model.PropertyValueProvider;
 import org.springframework.data.mapping.model.SpELContext;
-import org.springframework.data.mapping.model.SpELExpressionParameterValueProvider;
 import org.springframework.data.mapping.model.ValueExpressionEvaluator;
 import org.springframework.data.mapping.model.ValueExpressionParameterValueProvider;
 import org.springframework.data.mongodb.CodecRegistryProvider;
@@ -95,7 +96,7 @@
 import org.springframework.data.util.Predicates;
 import org.springframework.data.util.TypeInformation;
 import org.springframework.expression.spel.standard.SpelExpressionParser;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.CollectionUtils;
@@ -158,7 +159,7 @@ public class MappingMongoConverter extends AbstractMongoConverter
 
 	protected @Nullable ApplicationContext applicationContext;
 	protected @Nullable Environment environment;
-	protected MongoTypeMapper typeMapper;
+	protected @Nullable MongoTypeMapper typeMapper;
 	protected @Nullable String mapKeyDotReplacement = null;
 	protected @Nullable CodecRegistryProvider codecRegistryProvider;
 
@@ -308,7 +309,9 @@ public void setApplicationContext(ApplicationContext applicationContext) throws
 		this.environment = applicationContext.getEnvironment();
 		this.spELContext = new SpELContext(this.spELContext, applicationContext);
 		this.projectionFactory.setBeanFactory(applicationContext);
-		this.projectionFactory.setBeanClassLoader(applicationContext.getClassLoader());
+		if(applicationContext.getClassLoader() != null) {
+			this.projectionFactory.setBeanClassLoader(applicationContext.getClassLoader());
+		}
 
 		if (entityCallbacks == null) {
 			setEntityCallbacks(EntityCallbacks.create(applicationContext));
@@ -419,7 +422,7 @@ FieldName getFieldName(MongoPersistentProperty prop) {
 		return accessor.getBean();
 	}
 
-	private Object doReadOrProject(ConversionContext context, Bson source, TypeInformation<?> typeHint,
+	private Object doReadOrProject(ConversionContext context, @Nullable Bson source, TypeInformation<?> typeHint,
 			EntityProjection<?, ?> typeDescriptor) {
 
 		if (typeDescriptor.isProjection()) {
@@ -434,12 +437,12 @@ static class MapPersistentPropertyAccessor implements PersistentPropertyAccessor
 		Map<String, Object> map = new LinkedHashMap<>();
 
 		@Override
-		public void setProperty(PersistentProperty<?> persistentProperty, Object o) {
+		public void setProperty(PersistentProperty<?> persistentProperty, @Nullable Object o) {
 			map.put(persistentProperty.getName(), o);
 		}
 
 		@Override
-		public Object getProperty(PersistentProperty<?> persistentProperty) {
+		public @Nullable Object getProperty(PersistentProperty<?> persistentProperty) {
 			return map.get(persistentProperty.getName());
 		}
 
@@ -467,10 +470,14 @@ protected <S extends Object> S read(TypeInformation<S> type, Bson bson) {
 	 * @return the converted object, will never be {@literal null}.
 	 * @since 3.2
 	 */
-	@SuppressWarnings("unchecked")
-	protected <S extends Object> S readDocument(ConversionContext context, Bson bson,
+	@SuppressWarnings({"unchecked","NullAway"})
+	protected <S extends Object> S readDocument(ConversionContext context, @Nullable Bson bson,
 			TypeInformation<? extends S> typeHint) {
 
+		if(bson == null) {
+			bson  = new Document();
+		}
+
 		Document document = bson instanceof BasicDBObject dbObject ? new Document(dbObject) : (Document) bson;
 		TypeInformation<? extends S> typeToRead = getTypeMapper().readType(document, typeHint);
 		Class<? extends S> rawType = typeToRead.getType();
@@ -546,7 +553,7 @@ public EvaluatingDocumentAccessor(Bson document) {
 		}
 
 		@Override
-		public <T> T evaluate(String expression) {
+		public <T> @Nullable T evaluate(String expression) {
 			return expressionEvaluatorFactory.create(getDocument()).evaluate(expression);
 		}
 	}
@@ -600,8 +607,7 @@ private <S> S populateProperties(ConversionContext context, MongoPersistentEntit
 	 * Reads the identifier from either the bean backing the {@link PersistentPropertyAccessor} or the source document in
 	 * case the identifier has not be populated yet. In this case the identifier is set on the bean for further reference.
 	 */
-	@Nullable
-	private Object readAndPopulateIdentifier(ConversionContext context, PersistentPropertyAccessor<?> accessor,
+	private @Nullable Object readAndPopulateIdentifier(ConversionContext context, PersistentPropertyAccessor<?> accessor,
 			DocumentAccessor document, MongoPersistentEntity<?> entity, ValueExpressionEvaluator evaluator) {
 
 		Object rawId = document.getRawId(entity);
@@ -685,8 +691,7 @@ private DbRefResolverCallback getDbRefResolverCallback(ConversionContext context
 				(prop, bson, e, path) -> MappingMongoConverter.this.getValueInternal(context, prop, bson, e));
 	}
 
-	@Nullable
-	private Object readAssociation(Association<MongoPersistentProperty> association, DocumentAccessor documentAccessor,
+	private @Nullable Object readAssociation(Association<MongoPersistentProperty> association, DocumentAccessor documentAccessor,
 			DbRefProxyHandler handler, DbRefResolverCallback callback, ConversionContext context) {
 
 		MongoPersistentProperty property = association.getInverse();
@@ -746,8 +751,8 @@ && peek(collection) instanceof Document) {
 		}
 	}
 
-	@Nullable
-	private Object readUnwrapped(ConversionContext context, DocumentAccessor documentAccessor,
+	@SuppressWarnings("NullAway")
+	private @Nullable Object readUnwrapped(ConversionContext context, DocumentAccessor documentAccessor,
 			MongoPersistentProperty prop, MongoPersistentEntity<?> unwrappedEntity) {
 
 		if (prop.findAnnotation(Unwrapped.class).onEmpty().equals(OnEmpty.USE_EMPTY)) {
@@ -763,13 +768,14 @@ private Object readUnwrapped(ConversionContext context, DocumentAccessor documen
 	}
 
 	@Override
+	@SuppressWarnings("NullAway")
 	public DBRef toDBRef(Object object, @Nullable MongoPersistentProperty referringProperty) {
 
 		org.springframework.data.mongodb.core.mapping.DBRef annotation;
 
 		if (referringProperty != null) {
 			annotation = referringProperty.getDBRef();
-			Assert.isTrue(annotation != null, "The referenced property has to be mapped with @DBRef");
+			Assert.notNull(annotation, "The referenced property has to be mapped with @DBRef");
 		}
 
 		// DATAMONGO-913
@@ -781,6 +787,7 @@ public DBRef toDBRef(Object object, @Nullable MongoPersistentProperty referringP
 	}
 
 	@Override
+	@SuppressWarnings("NullAway")
 	public DocumentPointer toDocumentPointer(Object source, @Nullable MongoPersistentProperty referringProperty) {
 
 		if (source instanceof LazyLoadingProxy proxy) {
@@ -800,6 +807,7 @@ public DocumentPointer toDocumentPointer(Object source, @Nullable MongoPersisten
 		throw new IllegalArgumentException("The referringProperty is neither a DBRef nor a document reference");
 	}
 
+	@SuppressWarnings("NullAway")
 	DocumentPointer<?> createDocumentPointer(Object source, @Nullable MongoPersistentProperty referringProperty) {
 
 		if (referringProperty == null) {
@@ -864,7 +872,7 @@ private boolean requiresTypeHint(Class<?> type) {
 	/**
 	 * Internal write conversion method which should be used for nested invocations.
 	 */
-	@SuppressWarnings("unchecked")
+	@SuppressWarnings({"unchecked","NullAway"})
 	protected void writeInternal(@Nullable Object obj, Bson bson, @Nullable TypeInformation<?> typeHint) {
 
 		if (null == obj) {
@@ -1268,6 +1276,7 @@ protected String potentiallyEscapeMapKey(String source) {
 	 *
 	 * @param key
 	 */
+	@SuppressWarnings("NullAway")
 	private String potentiallyConvertMapKey(Object key) {
 
 		if (key instanceof String stringValue) {
@@ -1333,20 +1342,23 @@ private void writeSimpleInternal(@Nullable Object value, Bson bson, MongoPersist
 				property.hasExplicitWriteTarget() ? property.getFieldType() : Object.class));
 	}
 
-	@Nullable
 	@SuppressWarnings("unchecked")
-	private Object applyPropertyConversion(@Nullable Object value, MongoPersistentProperty property,
+	private @Nullable Object applyPropertyConversion(@Nullable Object value, MongoPersistentProperty property,
 			PersistentPropertyAccessor<?> persistentPropertyAccessor) {
 		MongoConversionContext context = new MongoConversionContext(new PropertyValueProvider<>() {
 
-			@Nullable
 			@Override
-			public <T> T getPropertyValue(MongoPersistentProperty property) {
+			public <T> @Nullable T getPropertyValue(MongoPersistentProperty property) {
 				return (T) persistentPropertyAccessor.getProperty(property);
 			}
 		}, property, this, spELContext);
-		PropertyValueConverter<Object, Object, ValueConversionContext<MongoPersistentProperty>> valueConverter = conversions
-				.getPropertyValueConversions().getValueConverter(property);
+
+		PropertyValueConversions propertyValueConversions = conversions.getPropertyValueConversions();
+		if(propertyValueConversions == null) {
+			return value;
+		}
+
+		PropertyValueConverter<Object, Object, ValueConversionContext<MongoPersistentProperty>> valueConverter = propertyValueConversions.getValueConverter(property);
 		return value != null ? valueConverter.write(value, context) : valueConverter.writeNull(context);
 	}
 
@@ -1354,8 +1366,9 @@ public <T> T getPropertyValue(MongoPersistentProperty property) {
 	 * Checks whether we have a custom conversion registered for the given value into an arbitrary simple Mongo type.
 	 * Returns the converted value if so. If not, we perform special enum handling or simply return the value as is.
 	 */
-	@Nullable
-	private Object getPotentiallyConvertedSimpleWrite(@Nullable Object value, @Nullable Class<?> typeHint) {
+	@Contract("null, _-> null")
+	@SuppressWarnings("NullAway")
+	private @Nullable Object getPotentiallyConvertedSimpleWrite(@Nullable Object value, @Nullable Class<?> typeHint) {
 
 		if (value == null) {
 			return null;
@@ -1391,7 +1404,7 @@ private Object getPotentiallyConvertedSimpleWrite(@Nullable Object value, @Nulla
 	 *
 	 * @since 3.2
 	 */
-	protected Object getPotentiallyConvertedSimpleRead(Object value, TypeInformation<?> target) {
+	protected @Nullable Object getPotentiallyConvertedSimpleRead(@Nullable Object value, TypeInformation<?> target) {
 		return getPotentiallyConvertedSimpleRead(value, target.getType());
 	}
 
@@ -1399,10 +1412,11 @@ protected Object getPotentiallyConvertedSimpleRead(Object value, TypeInformation
 	 * Checks whether we have a custom conversion for the given simple object. Converts the given value if so, applies
 	 * {@link Enum} handling or returns the value as is.
 	 */
+	@Contract("null, _ -> null; _, null -> param1")
 	@SuppressWarnings({ "rawtypes", "unchecked" })
-	private Object getPotentiallyConvertedSimpleRead(Object value, @Nullable Class<?> target) {
+	private @Nullable Object getPotentiallyConvertedSimpleRead(@Nullable Object value, @Nullable Class<?> target) {
 
-		if (target == null) {
+		if (target == null || value == null) {
 			return value;
 		}
 
@@ -1421,6 +1435,7 @@ private Object getPotentiallyConvertedSimpleRead(Object value, @Nullable Class<?
 		return doConvert(value, target);
 	}
 
+	@SuppressWarnings("NullAway")
 	protected DBRef createDBRef(Object target, @Nullable MongoPersistentProperty property) {
 
 		Assert.notNull(target, "Target object must not be null");
@@ -1456,8 +1471,7 @@ protected DBRef createDBRef(Object target, @Nullable MongoPersistentProperty pro
 		throw new MappingException("No id property found on class " + entity.getType());
 	}
 
-	@Nullable
-	private Object getValueInternal(ConversionContext context, MongoPersistentProperty prop, Bson bson,
+	private @Nullable Object getValueInternal(ConversionContext context, MongoPersistentProperty prop, Bson bson,
 			ValueExpressionEvaluator evaluator) {
 		return new MongoDbPropertyValueProvider(context, bson, evaluator).getPropertyValue(prop);
 	}
@@ -1472,11 +1486,12 @@ private Object getValueInternal(ConversionContext context, MongoPersistentProper
 	 * @since 3.2
 	 * @return the converted {@link Collection} or array, will never be {@literal null}.
 	 */
-	@SuppressWarnings("unchecked")
-	protected Object readCollectionOrArray(ConversionContext context, Collection<?> source,
+	@SuppressWarnings({"unchecked","NullAway"})
+	protected @Nullable Object readCollectionOrArray(ConversionContext context, @Nullable Collection<?> source,
 			TypeInformation<?> targetType) {
 
 		Assert.notNull(targetType, "Target type must not be null");
+		Assert.notNull(source, "Source must not be null");
 
 		Class<?> collectionType = targetType.isSubTypeOf(Collection.class) //
 				? targetType.getType() //
@@ -1518,7 +1533,7 @@ protected Object readCollectionOrArray(ConversionContext context, Collection<?>
 	 * @return the converted {@link Map}, will never be {@literal null}.
 	 * @since 3.2
 	 */
-	protected Map<Object, Object> readMap(ConversionContext context, Bson bson, TypeInformation<?> targetType) {
+	protected @Nullable Map<Object, Object> readMap(ConversionContext context, @Nullable Bson bson, TypeInformation<?> targetType) {
 
 		Assert.notNull(bson, "Document must not be null");
 		Assert.notNull(targetType, "TypeInformation must not be null");
@@ -1715,9 +1730,8 @@ private Object removeTypeInfo(Object object, boolean recursively) {
 		return document;
 	}
 
-	@Nullable
 	@SuppressWarnings("unchecked")
-	<T> T readValue(ConversionContext context, @Nullable Object value, TypeInformation<?> type) {
+	<T> @Nullable T readValue(ConversionContext context, @Nullable Object value, TypeInformation<?> type) {
 
 		if (value == null) {
 			return null;
@@ -1736,8 +1750,7 @@ <T> T readValue(ConversionContext context, @Nullable Object value, TypeInformati
 		return (T) context.convert(value, type);
 	}
 
-	@Nullable
-	private Object readDBRef(ConversionContext context, @Nullable DBRef dbref, TypeInformation<?> type) {
+	private @Nullable Object readDBRef(ConversionContext context, @Nullable DBRef dbref, TypeInformation<?> type) {
 
 		if (type.getType().equals(DBRef.class)) {
 			return dbref;
@@ -1802,6 +1815,7 @@ private <T> List<T> bulkReadAndConvertDBRefs(ConversionContext context, List<DBR
 		return targetList;
 	}
 
+	@SuppressWarnings("NullAway")
 	private void maybeEmitEvent(MongoMappingEvent<?> event) {
 
 		if (canPublishEvent()) {
@@ -1881,12 +1895,12 @@ public MappingMongoConverter with(MongoDatabaseFactory dbFactory) {
 		return target;
 	}
 
-	private <T extends Object> T doConvert(Object value, Class<? extends T> target) {
+	private <T extends Object> @Nullable T doConvert(Object value, Class<? extends T> target) {
 		return doConvert(value, target, null);
 	}
 
 	@SuppressWarnings("ConstantConditions")
-	private <T extends Object> T doConvert(Object value, Class<? extends T> target,
+	private <T extends Object> @Nullable T doConvert(Object value, Class<? extends T> target,
 			@Nullable Class<? extends T> fallback) {
 
 		if (conversionService.canConvert(value.getClass(), target) || fallback == null) {
@@ -1940,7 +1954,7 @@ static class MongoDbPropertyValueProvider implements PropertyValueProvider<Mongo
 		final ConversionContext context;
 		final DocumentAccessor accessor;
 		final ValueExpressionEvaluator evaluator;
-		final SpELContext spELContext;
+		final @Nullable SpELContext spELContext;
 
 		/**
 		 * Creates a new {@link MongoDbPropertyValueProvider} for the given source, {@link ValueExpressionEvaluator} and
@@ -1963,7 +1977,7 @@ static class MongoDbPropertyValueProvider implements PropertyValueProvider<Mongo
 		 * @param evaluator must not be {@literal null}.
 		 */
 		MongoDbPropertyValueProvider(ConversionContext context, DocumentAccessor accessor,
-				ValueExpressionEvaluator evaluator, SpELContext spELContext) {
+				ValueExpressionEvaluator evaluator, @Nullable SpELContext spELContext) {
 
 			this.context = context;
 			this.accessor = accessor;
@@ -1972,9 +1986,8 @@ static class MongoDbPropertyValueProvider implements PropertyValueProvider<Mongo
 		}
 
 		@Override
-		@Nullable
-		@SuppressWarnings("unchecked")
-		public <T> T getPropertyValue(MongoPersistentProperty property) {
+		@SuppressWarnings({"unchecked", "NullAway"})
+		public <T> @Nullable T getPropertyValue(MongoPersistentProperty property) {
 
 			String expression = property.getSpelExpression();
 			Object value = expression != null ? evaluator.evaluate(expression) : accessor.get(property);
@@ -2059,7 +2072,7 @@ public <T> T getPropertyValue(MongoPersistentProperty property) {
 	}
 
 	/**
-	 * Extension of {@link SpELExpressionParameterValueProvider} to recursively trigger value conversion on the raw
+	 * Extension of {@link ValueExpressionParameterValueProvider} to recursively trigger value conversion on the raw
 	 * resolved SpEL value.
 	 *
 	 * @author Oliver Gierke
@@ -2110,7 +2123,7 @@ enum NoOpParameterValueProvider implements ParameterValueProvider<MongoPersisten
 		INSTANCE;
 
 		@Override
-		public <T> T getParameterValue(Parameter<T, MongoPersistentProperty> parameter) {
+		public <T> @Nullable T getParameterValue(Parameter<T, MongoPersistentProperty> parameter) {
 			return null;
 		}
 	}
@@ -2138,7 +2151,7 @@ public List<org.springframework.data.util.TypeInformation<?>> getParameterTypes(
 		}
 
 		@Override
-		public org.springframework.data.util.TypeInformation<?> getProperty(String property) {
+		public org.springframework.data.util.@Nullable TypeInformation<?> getProperty(String property) {
 			return delegate.getProperty(property);
 		}
 
@@ -2173,7 +2186,7 @@ public TypeInformation<?> getRawTypeInformation() {
 		}
 
 		@Override
-		public org.springframework.data.util.TypeInformation<?> getActualType() {
+		public org.springframework.data.util.@Nullable TypeInformation<?> getActualType() {
 			return delegate.getActualType();
 		}
 
@@ -2188,7 +2201,7 @@ public List<org.springframework.data.util.TypeInformation<?>> getParameterTypes(
 		}
 
 		@Override
-		public org.springframework.data.util.TypeInformation<?> getSuperTypeInformation(Class superType) {
+		public org.springframework.data.util.@Nullable TypeInformation<?> getSuperTypeInformation(Class superType) {
 			return delegate.getSuperTypeInformation(superType);
 		}
 
@@ -2211,6 +2224,11 @@ public org.springframework.data.util.TypeInformation<? extends S> specialize(Typ
 		public TypeDescriptor toTypeDescriptor() {
 			return delegate.toTypeDescriptor();
 		}
+
+		@Override
+		public ResolvableType toResolvableType() {
+			return delegate.toResolvableType();
+		}
 	}
 
 	/**
@@ -2280,8 +2298,7 @@ default ConversionContext forProperty(MongoPersistentProperty property) {
 		 * @return
 		 * @param <S>
 		 */
-		@Nullable
-		default <S> S findContextualEntity(MongoPersistentEntity<S> entity, Document document) {
+		default <S> @Nullable S findContextualEntity(MongoPersistentEntity<S> entity, Document document) {
 			return null;
 		}
 
@@ -2315,7 +2332,7 @@ public ConversionContext withPath(ObjectPath currentPath) {
 		}
 
 		@Override
-		public <S> S findContextualEntity(MongoPersistentEntity<S> entity, Document document) {
+		public <S> @Nullable S findContextualEntity(MongoPersistentEntity<S> entity, Document document) {
 
 			Object identifier = document.get(BasicMongoPersistentProperty.ID_FIELD_NAME);
 
@@ -2352,15 +2369,15 @@ protected static class DefaultConversionContext implements ConversionContext {
 		final ObjectPath path;
 		final ContainerValueConverter<Bson> documentConverter;
 		final ContainerValueConverter<Collection<?>> collectionConverter;
-		final ContainerValueConverter<Bson> mapConverter;
-		final ContainerValueConverter<DBRef> dbRefConverter;
-		final ValueConverter<Object> elementConverter;
+		final ContainerValueConverter<@Nullable Bson> mapConverter;
+		final ContainerValueConverter<@Nullable DBRef> dbRefConverter;
+		final ValueConverter<@Nullable Object> elementConverter;
 
 		DefaultConversionContext(MongoConverter sourceConverter,
 				org.springframework.data.convert.CustomConversions customConversions, ObjectPath path,
-				ContainerValueConverter<Bson> documentConverter, ContainerValueConverter<Collection<?>> collectionConverter,
-				ContainerValueConverter<Bson> mapConverter, ContainerValueConverter<DBRef> dbRefConverter,
-				ValueConverter<Object> elementConverter) {
+				ContainerValueConverter<@Nullable Bson> documentConverter, ContainerValueConverter<@Nullable Collection<?>> collectionConverter,
+				ContainerValueConverter<@Nullable Bson> mapConverter, ContainerValueConverter<@Nullable DBRef> dbRefConverter,
+				ValueConverter<@Nullable Object> elementConverter) {
 
 			this.sourceConverter = sourceConverter;
 			this.conversions = customConversions;
@@ -2372,8 +2389,8 @@ protected static class DefaultConversionContext implements ConversionContext {
 			this.elementConverter = elementConverter;
 		}
 
-		@SuppressWarnings("unchecked")
 		@Override
+		@SuppressWarnings({"unchecked", "NullAway"})
 		public <S extends Object> S convert(Object source, TypeInformation<? extends S> typeHint,
 				ConversionContext context) {
 
@@ -2454,7 +2471,7 @@ public ObjectPath getPath() {
 		 */
 		interface ValueConverter<T> {
 
-			Object convert(T source, TypeInformation<?> typeHint);
+			@Nullable Object convert(@Nullable T source, TypeInformation<?> typeHint);
 
 		}
 
@@ -2466,7 +2483,7 @@ interface ValueConverter<T> {
 		 */
 		interface ContainerValueConverter<T> {
 
-			Object convert(ConversionContext context, T source, TypeInformation<?> typeHint);
+			@Nullable Object convert(ConversionContext context, @Nullable T source, TypeInformation<?> typeHint);
 
 		}
 
@@ -2481,7 +2498,7 @@ class ProjectingConversionContext extends DefaultConversionContext {
 
 		ProjectingConversionContext(MongoConverter sourceConverter, CustomConversions customConversions, ObjectPath path,
 				ContainerValueConverter<Collection<?>> collectionConverter, ContainerValueConverter<Bson> mapConverter,
-				ContainerValueConverter<DBRef> dbRefConverter, ValueConverter<Object> elementConverter,
+				ContainerValueConverter<@Nullable DBRef> dbRefConverter, ValueConverter<@Nullable Object> elementConverter,
 				EntityProjection<?, ?> projection) {
 			super(sourceConverter, customConversions, path,
 					(context, source, typeHint) -> doReadOrProject(context, source, typeHint, projection),
@@ -2533,7 +2550,7 @@ public void setProperty(PersistentProperty<?> property, @Nullable Object value)
 		}
 
 		@Override
-		public Object getProperty(PersistentProperty<?> property) {
+		public @Nullable Object getProperty(PersistentProperty<?> property) {
 			return delegate.getProperty(translate(property));
 		}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java
index 5fde0acddd..0cc687c815 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java
@@ -16,41 +16,56 @@
 package org.springframework.data.mongodb.core.convert;
 
 import org.bson.conversions.Bson;
-
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.convert.ValueConversionContext;
 import org.springframework.data.mapping.model.PropertyValueProvider;
 import org.springframework.data.mapping.model.SpELContext;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
 import org.springframework.data.util.TypeInformation;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.CheckReturnValue;
 
 /**
  * {@link ValueConversionContext} that allows to delegate read/write to an underlying {@link MongoConverter}.
  *
  * @author Christoph Strobl
+ * @author Ross Lawley
  * @since 3.4
  */
 public class MongoConversionContext implements ValueConversionContext<MongoPersistentProperty> {
 
 	private final PropertyValueProvider<MongoPersistentProperty> accessor; // TODO: generics
-	private final @Nullable MongoPersistentProperty persistentProperty;
 	private final MongoConverter mongoConverter;
 
+	@Nullable private final MongoPersistentProperty persistentProperty;
 	@Nullable private final SpELContext spELContext;
+	@Nullable private final OperatorContext operatorContext;
 
 	public MongoConversionContext(PropertyValueProvider<MongoPersistentProperty> accessor,
 			@Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter) {
-		this(accessor, persistentProperty, mongoConverter, null);
+		this(accessor, persistentProperty, mongoConverter, null, null);
 	}
 
 	public MongoConversionContext(PropertyValueProvider<MongoPersistentProperty> accessor,
 			@Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter,
 			@Nullable SpELContext spELContext) {
+		this(accessor, persistentProperty, mongoConverter, spELContext, null);
+	}
+
+	public MongoConversionContext(PropertyValueProvider<MongoPersistentProperty> accessor,
+			@Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter,
+			@Nullable OperatorContext operatorContext) {
+		this(accessor, persistentProperty, mongoConverter, null, operatorContext);
+	}
+
+	public MongoConversionContext(PropertyValueProvider<MongoPersistentProperty> accessor,
+			@Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter,
+			@Nullable SpELContext spELContext, @Nullable OperatorContext operatorContext) {
 
 		this.accessor = accessor;
 		this.persistentProperty = persistentProperty;
 		this.mongoConverter = mongoConverter;
 		this.spELContext = spELContext;
+		this.operatorContext = operatorContext;
 	}
 
 	@Override
@@ -63,6 +78,16 @@ public MongoPersistentProperty getProperty() {
 		return persistentProperty;
 	}
 
+	/**
+	 * @param operatorContext
+	 * @return new instance of {@link MongoConversionContext}.
+	 * @since 4.5
+	 */
+	@CheckReturnValue
+	public MongoConversionContext forOperator(@Nullable OperatorContext operatorContext) {
+		return new MongoConversionContext(accessor, persistentProperty, mongoConverter, spELContext, operatorContext);
+	}
+
 	@Nullable
 	public Object getValue(String propertyPath) {
 		return accessor.getPropertyValue(getProperty().getOwner().getRequiredPersistentProperty(propertyPath));
@@ -70,12 +95,12 @@ public Object getValue(String propertyPath) {
 
 	@Override
 	@SuppressWarnings("unchecked")
-	public <T> T write(@Nullable Object value, TypeInformation<T> target) {
+	public <T> @Nullable T write(@Nullable Object value, TypeInformation<T> target) {
 		return (T) mongoConverter.convertToMongoType(value, target);
 	}
 
 	@Override
-	public <T> T read(@Nullable Object value, TypeInformation<T> target) {
+	public <T> @Nullable T read(@Nullable Object value, TypeInformation<T> target) {
 		return value instanceof Bson bson ? mongoConverter.read(target.getType(), bson)
 				: ValueConversionContext.super.read(value, target);
 	}
@@ -84,4 +109,62 @@ public <T> T read(@Nullable Object value, TypeInformation<T> target) {
 	public SpELContext getSpELContext() {
 		return spELContext;
 	}
+
+	@Nullable
+	public OperatorContext getOperatorContext() {
+		return operatorContext;
+	}
+
+	/**
+	 * The {@link OperatorContext} provides access to the actual conversion intent like a write operation or a query
+	 * operator such as {@literal $gte}.
+	 *
+	 * @since 4.5
+	 */
+	public interface OperatorContext {
+
+		/**
+		 * The operator the conversion is used in.
+		 *
+		 * @return {@literal write} for simple write operations during save, or a query operator.
+		 */
+		String operator();
+
+		/**
+		 * The context path the operator is used in.
+		 *
+		 * @return never {@literal null}.
+		 */
+		String path();
+
+		boolean isWriteOperation();
+
+	}
+
+	record WriteOperatorContext(String path) implements OperatorContext {
+
+		@Override
+		public String operator() {
+			return "write";
+		}
+
+		@Override
+		public boolean isWriteOperation() {
+			return true;
+		}
+	}
+
+	record QueryOperatorContext(String operator, String path) implements OperatorContext {
+
+		public QueryOperatorContext(@Nullable String operator, String path) {
+			this.operator = operator != null ? operator : "$eq";
+			this.path = path;
+		}
+
+		@Override
+		public boolean isWriteOperation() {
+			return false;
+		}
+	}
+
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverter.java
index 3676e74c8b..e147d64cc5 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverter.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverter.java
@@ -21,6 +21,7 @@
 import org.bson.codecs.configuration.CodecRegistry;
 import org.bson.conversions.Bson;
 import org.bson.types.ObjectId;
+import org.jspecify.annotations.Nullable;
 import org.springframework.core.convert.ConversionException;
 import org.springframework.data.convert.CustomConversions;
 import org.springframework.data.convert.EntityConverter;
@@ -33,7 +34,6 @@
 import org.springframework.data.projection.EntityProjection;
 import org.springframework.data.projection.ProjectionFactory;
 import org.springframework.data.util.TypeInformation;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 
@@ -101,9 +101,8 @@ public interface MongoConverter
 	 * @throws IllegalArgumentException if {@literal targetType} is {@literal null}.
 	 * @since 2.1
 	 */
-	@SuppressWarnings("unchecked")
-	@Nullable
-	default <S, T> T mapValueToTargetType(S source, Class<T> targetType, DbRefResolver dbRefResolver) {
+	@SuppressWarnings({"unchecked","NullAway"})
+	default <S, T> @Nullable T mapValueToTargetType(S source, Class<T> targetType, DbRefResolver dbRefResolver) {
 
 		Assert.notNull(targetType, "TargetType must not be null");
 		Assert.notNull(dbRefResolver, "DbRefResolver must not be null");
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java
index f9a67d73a0..1fd45e1960 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java
@@ -47,6 +47,7 @@
 import org.bson.types.Code;
 import org.bson.types.Decimal128;
 import org.bson.types.ObjectId;
+import org.jspecify.annotations.Nullable;
 
 import org.springframework.core.convert.ConversionFailedException;
 import org.springframework.core.convert.TypeDescriptor;
@@ -142,7 +143,7 @@ public String convert(ObjectId id) {
 	enum StringToObjectIdConverter implements Converter<String, ObjectId> {
 		INSTANCE;
 
-		public ObjectId convert(String source) {
+		public @Nullable ObjectId convert(String source) {
 			return StringUtils.hasText(source) ? new ObjectId(source) : null;
 		}
 	}
@@ -206,7 +207,7 @@ public Decimal128 convert(BigInteger source) {
 	enum StringToBigDecimalConverter implements Converter<String, BigDecimal> {
 		INSTANCE;
 
-		public BigDecimal convert(String source) {
+		public @Nullable BigDecimal convert(String source) {
 			return StringUtils.hasText(source) ? new BigDecimal(source) : null;
 		}
 	}
@@ -235,7 +236,7 @@ public String convert(BigInteger source) {
 	enum StringToBigIntegerConverter implements Converter<String, BigInteger> {
 		INSTANCE;
 
-		public BigInteger convert(String source) {
+		public @Nullable BigInteger convert(String source) {
 			return StringUtils.hasText(source) ? new BigInteger(source) : null;
 		}
 	}
@@ -312,20 +313,25 @@ public String convert(Term source) {
 	 * @author Christoph Strobl
 	 * @since 1.7
 	 */
+	@SuppressWarnings("NullAway")
 	enum DocumentToNamedMongoScriptConverter implements Converter<Document, NamedMongoScript> {
 
 		INSTANCE;
 
 		@Override
-		public NamedMongoScript convert(Document source) {
+		public @Nullable NamedMongoScript convert(Document source) {
 
 			if (source.isEmpty()) {
 				return null;
 			}
 
 			String id = source.get(FieldName.ID.name()).toString();
+			Assert.notNull(id, "Script id must not be null");
+
 			Object rawValue = source.get("value");
 
+			Assert.isInstanceOf(Code.class, rawValue);
+
 			return new NamedMongoScript(id, ((Code) rawValue).getCode());
 		}
 	}
@@ -379,7 +385,7 @@ enum StringToCurrencyConverter implements Converter<String, Currency> {
 		INSTANCE;
 
 		@Override
-		public Currency convert(String source) {
+		public @Nullable Currency convert(String source) {
 			return StringUtils.hasText(source) ? Currency.getInstance(source) : null;
 		}
 	}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java
index 050c3bd27d..8dccced380 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java
@@ -32,6 +32,7 @@
 import java.util.Set;
 import java.util.function.Consumer;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.core.convert.TypeDescriptor;
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.core.convert.converter.ConverterFactory;
@@ -51,7 +52,7 @@
 import org.springframework.data.mongodb.core.convert.MongoConverters.StringToBigIntegerConverter;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
 import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 
 /**
@@ -137,7 +138,7 @@ public Set<ConvertiblePair> getConvertibleTypes() {
 			return new HashSet<>(Arrays.asList(localeToString, booleanToString));
 		}
 
-		public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
+		public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
 			return source != null ? source.toString() : null;
 		}
 	}
@@ -188,6 +189,7 @@ public static MongoConverterConfigurationAdapter from(List<?> converters) {
 		 * @param converter must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public MongoConverterConfigurationAdapter registerConverter(Converter<?, ?> converter) {
 
 			Assert.notNull(converter, "Converter must not be null");
@@ -202,6 +204,7 @@ public MongoConverterConfigurationAdapter registerConverter(Converter<?, ?> conv
 		 * @param converters must not be {@literal null} nor contain {@literal null} values.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public MongoConverterConfigurationAdapter registerConverters(Collection<?> converters) {
 
 			Assert.notNull(converters, "Converters must not be null");
@@ -217,6 +220,7 @@ public MongoConverterConfigurationAdapter registerConverters(Collection<?> conve
 		 * @param converterFactory must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public MongoConverterConfigurationAdapter registerConverterFactory(ConverterFactory<?, ?> converterFactory) {
 
 			Assert.notNull(converterFactory, "ConverterFactory must not be null");
@@ -232,6 +236,7 @@ public MongoConverterConfigurationAdapter registerConverterFactory(ConverterFact
 		 * @return this.
 		 * @since 3.4
 		 */
+		@Contract("_ -> this")
 		public MongoConverterConfigurationAdapter registerPropertyValueConverterFactory(
 				PropertyValueConverterFactory converterFactory) {
 
@@ -249,6 +254,7 @@ public MongoConverterConfigurationAdapter registerPropertyValueConverterFactory(
 		 * @return this.
 		 * @since 3.4
 		 */
+		@Contract("_ -> this")
 		public MongoConverterConfigurationAdapter configurePropertyConversions(
 				Consumer<PropertyValueConverterRegistrar<MongoPersistentProperty>> configurationAdapter) {
 
@@ -271,6 +277,7 @@ public MongoConverterConfigurationAdapter configurePropertyConversions(
 		 * @param useNativeDriverJavaTimeCodecs
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public MongoConverterConfigurationAdapter useNativeDriverJavaTimeCodecs(boolean useNativeDriverJavaTimeCodecs) {
 
 			this.useNativeDriverJavaTimeCodecs = useNativeDriverJavaTimeCodecs;
@@ -285,6 +292,7 @@ public MongoConverterConfigurationAdapter useNativeDriverJavaTimeCodecs(boolean
 		 * @return this.
 		 * @see #useNativeDriverJavaTimeCodecs(boolean)
 		 */
+		@Contract("-> this")
 		public MongoConverterConfigurationAdapter useNativeDriverJavaTimeCodecs() {
 			return useNativeDriverJavaTimeCodecs(true);
 		}
@@ -299,6 +307,7 @@ public MongoConverterConfigurationAdapter useNativeDriverJavaTimeCodecs() {
 		 * @return this.
 		 * @see #useNativeDriverJavaTimeCodecs(boolean)
 		 */
+		@Contract("-> this")
 		public MongoConverterConfigurationAdapter useSpringDataJavaTimeCodecs() {
 			return useNativeDriverJavaTimeCodecs(false);
 		}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java
index 0316251dc1..67f9d5ec46 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java
@@ -28,6 +28,7 @@
 import java.util.regex.Pattern;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.domain.Example;
 import org.springframework.data.domain.ExampleMatcher.NullHandler;
 import org.springframework.data.domain.ExampleMatcher.PropertyValueTransformer;
@@ -98,13 +99,17 @@ public Document getMappedExample(Example<?> example) {
 	 * @param entity must not be {@literal null}.
 	 * @return
 	 */
-	public Document getMappedExample(Example<?> example, MongoPersistentEntity<?> entity) {
+	@SuppressWarnings("NullAway")
+	public Document getMappedExample(Example<?> example, @Nullable MongoPersistentEntity<?> entity) {
 
 		Assert.notNull(example, "Example must not be null");
-		Assert.notNull(entity, "MongoPersistentEntity must not be null");
 
 		Document reference = (Document) converter.convertToMongoType(example.getProbe());
 
+		if(entity != null) {
+			entity = mappingContext.getRequiredPersistentEntity(example.getProbeType());
+		}
+
 		if (entity.getIdProperty() != null && ClassUtils.isAssignable(entity.getType(), example.getProbeType())) {
 
 			Object identifier = entity.getIdentifierAccessor(example.getProbe()).getIdentifier();
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoJsonSchemaMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoJsonSchemaMapper.java
index 8d199083e7..a5d329045e 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoJsonSchemaMapper.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoJsonSchemaMapper.java
@@ -21,12 +21,12 @@
 import java.util.stream.Collectors;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mapping.PersistentEntity;
 import org.springframework.data.mapping.context.MappingContext;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
 import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
 /**
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java
index 867a6213d2..8aeb576c67 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java
@@ -16,12 +16,12 @@
 package org.springframework.data.mongodb.core.convert;
 
 import org.bson.conversions.Bson;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.convert.EntityWriter;
 import org.springframework.data.mongodb.core.mapping.DocumentPointer;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
 import org.springframework.data.util.TypeInformation;
-import org.springframework.lang.Nullable;
 
 import com.mongodb.DBRef;
 
@@ -43,8 +43,7 @@ public interface MongoWriter<T> extends EntityWriter<T, Bson> {
 	 * @param obj can be {@literal null}.
 	 * @return
 	 */
-	@Nullable
-	default Object convertToMongoType(@Nullable Object obj) {
+	default @Nullable Object convertToMongoType(@Nullable Object obj) {
 		return convertToMongoType(obj, (TypeInformation<?>) null);
 	}
 
@@ -59,7 +58,7 @@ default Object convertToMongoType(@Nullable Object obj) {
 	@Nullable
 	Object convertToMongoType(@Nullable Object obj, @Nullable TypeInformation<?> typeInformation);
 
-	default Object convertToMongoType(@Nullable Object obj, MongoPersistentEntity<?> entity) {
+	default @Nullable Object convertToMongoType(@Nullable Object obj, MongoPersistentEntity<?> entity) {
 		return convertToMongoType(obj, entity.getTypeInformation());
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/NoOpDbRefResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/NoOpDbRefResolver.java
index 265257af5c..68578f32b9 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/NoOpDbRefResolver.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/NoOpDbRefResolver.java
@@ -18,9 +18,8 @@
 import java.util.List;
 
 import org.bson.Document;
-
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
-import org.springframework.lang.Nullable;
 
 import com.mongodb.DBRef;
 
@@ -37,16 +36,14 @@ public enum NoOpDbRefResolver implements DbRefResolver {
 	INSTANCE;
 
 	@Override
-	@Nullable
-	public Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbref, DbRefResolverCallback callback,
+	public @Nullable Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbref, DbRefResolverCallback callback,
 			DbRefProxyHandler proxyHandler) {
 
 		return handle();
 	}
 
 	@Override
-	@Nullable
-	public Document fetch(DBRef dbRef) {
+	public @Nullable Document fetch(DBRef dbRef) {
 		return handle();
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ObjectPath.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ObjectPath.java
index 5fefd472c4..d5f034eb1d 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ObjectPath.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ObjectPath.java
@@ -18,9 +18,9 @@
 import java.util.ArrayList;
 import java.util.List;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
 import org.springframework.data.util.Lazy;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.ObjectUtils;
@@ -99,8 +99,7 @@ ObjectPath push(Object object, MongoPersistentEntity<?> entity, @Nullable Object
 	 * @return {@literal null} when no match found.
 	 * @since 2.0
 	 */
-	@Nullable
-	<T> T getPathItem(Object id, String collection, Class<T> type) {
+	<T> @Nullable T getPathItem(Object id, String collection, Class<T> type) {
 
 		Assert.notNull(id, "Id must not be null");
 		Assert.hasText(collection, "Collection name must not be null");
@@ -133,13 +132,11 @@ Object getCurrentObject() {
 		return getObject();
 	}
 
-	@Nullable
-	private Object getObject() {
+	private @Nullable Object getObject() {
 		return object;
 	}
 
-	@Nullable
-	private Object getIdValue() {
+	private @Nullable Object getIdValue() {
 		return idValue;
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java
index cce809adc6..11ed30aedd 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java
@@ -37,7 +37,8 @@
 import org.bson.Document;
 import org.bson.conversions.Bson;
 import org.bson.types.ObjectId;
-
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
 import org.springframework.core.convert.ConversionService;
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.data.annotation.Reference;
@@ -58,6 +59,8 @@
 import org.springframework.data.mongodb.core.aggregation.AggregationExpression;
 import org.springframework.data.mongodb.core.aggregation.RelaxedTypeBasedAggregationOperationContext;
 import org.springframework.data.mongodb.core.convert.MappingMongoConverter.NestedDocument;
+import org.springframework.data.mongodb.core.convert.MongoConversionContext.OperatorContext;
+import org.springframework.data.mongodb.core.convert.MongoConversionContext.QueryOperatorContext;
 import org.springframework.data.mongodb.core.mapping.FieldName;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
@@ -66,7 +69,7 @@
 import org.springframework.data.mongodb.util.BsonUtils;
 import org.springframework.data.mongodb.util.DotPath;
 import org.springframework.data.util.TypeInformation;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
@@ -88,6 +91,7 @@
  * @author David Julia
  * @author Divya Srivastava
  * @author Gyungrai Wang
+ * @author Ross Lawley
  */
 public class QueryMapper {
 
@@ -135,6 +139,7 @@ public Document getMappedObject(Bson query, Optional<? extends MongoPersistentEn
 	 * @param entity can be {@literal null}.
 	 * @return
 	 */
+	@SuppressWarnings("NullAway")
 	public Document getMappedObject(Bson query, @Nullable MongoPersistentEntity<?> entity) {
 
 		if (isNestedKeyword(query)) {
@@ -275,6 +280,7 @@ public Document addMetaAttributes(Document source, @Nullable MongoPersistentEnti
 		return mapMetaAttributes(source, entity, MetaMapping.FORCE);
 	}
 
+	@SuppressWarnings("NullAway")
 	private Document mapMetaAttributes(Document source, @Nullable MongoPersistentEntity<?> entity,
 			MetaMapping metaMapping) {
 
@@ -347,7 +353,7 @@ private Document getMappedTextScoreField(MongoPersistentProperty property) {
 	 * @param rawValue
 	 * @return
 	 */
-	protected Entry<String, Object> getMappedObjectForField(Field field, Object rawValue) {
+	protected Entry<String, @Nullable Object> getMappedObjectForField(Field field, @Nullable Object rawValue) {
 
 		String key = field.getMappedKey();
 		Object value;
@@ -411,7 +417,9 @@ protected Document getMappedKeyword(Keyword keyword, @Nullable MongoPersistentEn
 		}
 
 		if (keyword.isSample()) {
-			return exampleMapper.getMappedExample(keyword.getValue(), entity);
+
+			Example<?> example = keyword.getValue();
+			return exampleMapper.getMappedExample(example, entity != null ? entity : mappingContext.getRequiredPersistentEntity(example.getProbeType()));
 		}
 
 		if (keyword.isJsonSchema()) {
@@ -453,8 +461,8 @@ protected Document getMappedKeyword(Field property, Keyword keyword) {
 	 * @return
 	 */
 	@Nullable
-	@SuppressWarnings("unchecked")
-	protected Object getMappedValue(Field documentField, Object sourceValue) {
+	@SuppressWarnings("NullAway")
+	protected Object getMappedValue(Field documentField, @Nullable Object sourceValue) {
 
 		Object value = applyFieldTargetTypeHintToValue(documentField, sourceValue);
 
@@ -491,6 +499,7 @@ private boolean isIdField(Field documentField) {
 				&& documentField.getProperty().getOwner().isIdProperty(documentField.getProperty());
 	}
 
+	@SuppressWarnings("NullAway")
 	private Class<?> getIdTypeForField(Field documentField) {
 		return isIdField(documentField) ? documentField.getProperty().getFieldType() : ObjectId.class;
 	}
@@ -529,7 +538,7 @@ protected boolean isAssociationConversionNecessary(Field documentField, @Nullabl
 		}
 
 		MongoPersistentEntity<?> entity = documentField.getPropertyEntity();
-		return entity.hasIdProperty()
+		return entity != null && entity.hasIdProperty()
 				&& (type.equals(DBRef.class) || entity.getRequiredIdProperty().getActualType().isAssignableFrom(type));
 	}
 
@@ -665,14 +674,31 @@ protected Object convertAssociation(@Nullable Object source, @Nullable MongoPers
 		return createReferenceFor(source, property);
 	}
 
-	@Nullable
-	private Object convertValue(Field documentField, Object sourceValue, Object value,
+	private @Nullable Object convertValue(Field documentField, @Nullable Object sourceValue, @Nullable Object value,
 			PropertyValueConverter<Object, Object, ValueConversionContext<MongoPersistentProperty>> valueConverter) {
 
 		MongoPersistentProperty property = documentField.getProperty();
-		MongoConversionContext conversionContext = new MongoConversionContext(NoPropertyPropertyValueProvider.INSTANCE,
-				property, converter);
 
+		OperatorContext criteriaContext = new QueryOperatorContext(
+				isKeyword(documentField.name) ? documentField.name : "$eq", property != null ? property.getFieldName() : documentField.name);
+
+		MongoConversionContext conversionContext;
+		if (valueConverter instanceof MongoConversionContext mcc) {
+			conversionContext = mcc.forOperator(criteriaContext);
+		} else {
+			conversionContext = new MongoConversionContext(NoPropertyPropertyValueProvider.INSTANCE, property, converter,
+					criteriaContext);
+		}
+
+		return convertValueWithConversionContext(documentField, sourceValue, value, valueConverter, conversionContext);
+	}
+
+	@SuppressWarnings("NullAway")
+	protected @Nullable Object convertValueWithConversionContext(Field documentField, @Nullable Object sourceValue, @Nullable Object value,
+			PropertyValueConverter<Object, Object, ValueConversionContext<MongoPersistentProperty>> valueConverter,
+			MongoConversionContext conversionContext) {
+
+		MongoPersistentProperty property = documentField.getProperty();
 		/* might be an $in clause with multiple entries */
 		if (property != null && !property.isCollectionLike() && sourceValue instanceof Collection<?> collection) {
 
@@ -688,21 +714,22 @@ private Object convertValue(Field documentField, Object sourceValue, Object valu
 			return converted;
 		}
 
-		if (property != null && !documentField.getProperty().isMap() && sourceValue instanceof Document document) {
+		if (property != null && !property.isMap() && sourceValue instanceof Document document) {
 
 			return BsonUtils.mapValues(document, (key, val) -> {
 				if (isKeyword(key)) {
-					return getMappedValue(documentField, val);
+					return convertValueWithConversionContext(documentField, val, val, valueConverter, conversionContext
+							.forOperator(new QueryOperatorContext(key, conversionContext.getOperatorContext().path())));
 				}
 				return val;
 			});
 		}
 
-		return valueConverter.write(value, conversionContext);
+		return value != null ? valueConverter.write(value, conversionContext) : value;
 	}
 
 	@Nullable
-	@SuppressWarnings("unchecked")
+	@SuppressWarnings({"unchecked", "NullAway"})
 	private Object convertIdField(Field documentField, Object source) {
 
 		Object value = source;
@@ -836,6 +863,7 @@ public Object convertId(@Nullable Object id, Class<?> targetType) {
 	 * @param candidate
 	 * @return
 	 */
+	@Contract("null -> false")
 	protected boolean isNestedKeyword(@Nullable Object candidate) {
 
 		if (!(candidate instanceof Document)) {
@@ -870,8 +898,8 @@ protected boolean isTypeKey(String key) {
 	 * @param candidate
 	 * @return
 	 */
-	protected boolean isKeyword(String candidate) {
-		return candidate.startsWith("$");
+	protected boolean isKeyword(@Nullable String candidate) {
+		return candidate != null && candidate.startsWith("$");
 	}
 
 	/**
@@ -916,6 +944,7 @@ private Object applyFieldTargetTypeHintToValue(Field documentField, @Nullable Ob
 	 * @author Oliver Gierke
 	 * @author Christoph Strobl
 	 */
+	@SuppressWarnings("NullAway")
 	static class Keyword {
 
 		private static final Set<String> NON_DBREF_CONVERTING_KEYWORDS = Set.of("$", "$size", "$slice", "$gt", "$lt");
@@ -1164,6 +1193,7 @@ public MetadataBackedField(String name, MongoPersistentEntity<?> entity,
 		 * @param context must not be {@literal null}.
 		 * @param property may be {@literal null}.
 		 */
+		@SuppressWarnings("NullAway")
 		public MetadataBackedField(String name, MongoPersistentEntity<?> entity,
 				MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> context,
 				@Nullable MongoPersistentProperty property) {
@@ -1207,7 +1237,7 @@ public MongoPersistentProperty getProperty() {
 		}
 
 		@Override
-		public MongoPersistentEntity<?> getPropertyEntity() {
+		public @Nullable MongoPersistentEntity<?> getPropertyEntity() {
 			MongoPersistentProperty property = getProperty();
 			return property == null ? null : mappingContext.getPersistentEntity(property);
 		}
@@ -1224,7 +1254,7 @@ public boolean isAssociation() {
 		}
 
 		@Override
-		public Association<MongoPersistentProperty> getAssociation() {
+		public @Nullable Association<MongoPersistentProperty> getAssociation() {
 			return association;
 		}
 
@@ -1427,6 +1457,7 @@ protected Converter<MongoPersistentProperty, String> getPropertyConverter() {
 		 * @return
 		 * @since 1.7
 		 */
+		@SuppressWarnings("NullAway")
 		protected Converter<MongoPersistentProperty, String> getAssociationConverter() {
 			return new AssociationConverter(name, getAssociation());
 		}
@@ -1578,7 +1609,7 @@ public AssociationConverter(String name, Association<MongoPersistentProperty> as
 		}
 
 		@Override
-		public String convert(MongoPersistentProperty source) {
+		public @Nullable String convert(MongoPersistentProperty source) {
 
 			if (associationFound) {
 				return null;
@@ -1606,7 +1637,7 @@ public MongoConverter getConverter() {
 		return converter;
 	}
 
-	private enum NoPropertyPropertyValueProvider implements PropertyValueProvider<MongoPersistentProperty> {
+	enum NoPropertyPropertyValueProvider implements PropertyValueProvider<MongoPersistentProperty> {
 
 		INSTANCE;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLoader.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLoader.java
index 5a1adf9114..cd7d55311d 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLoader.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLoader.java
@@ -20,9 +20,9 @@
 
 import org.bson.Document;
 import org.bson.conversions.Bson;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceCollection;
 import org.springframework.data.mongodb.core.mapping.FieldName;
-import org.springframework.lang.Nullable;
 
 import com.mongodb.client.MongoCollection;
 
@@ -42,8 +42,7 @@ public interface ReferenceLoader {
 	 * @param context must not be {@literal null}.
 	 * @return the matching {@link Document} or {@literal null} if none found.
 	 */
-	@Nullable
-	default Document fetchOne(DocumentReferenceQuery referenceQuery, ReferenceCollection context) {
+	default @Nullable Document fetchOne(DocumentReferenceQuery referenceQuery, ReferenceCollection context) {
 
 		Iterator<Document> it = fetchMany(referenceQuery, context).iterator();
 		return it.hasNext() ? it.next() : null;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLookupDelegate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLookupDelegate.java
index b912cfb540..a0e5a6f2bb 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLookupDelegate.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLookupDelegate.java
@@ -31,6 +31,7 @@
 
 import org.bson.Document;
 import org.bson.conversions.Bson;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mapping.context.MappingContext;
 import org.springframework.data.mapping.model.SpELContext;
 import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery;
@@ -47,7 +48,6 @@
 import org.springframework.data.mongodb.util.spel.ExpressionUtils;
 import org.springframework.data.util.Streamable;
 import org.springframework.expression.EvaluationContext;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
@@ -98,8 +98,7 @@ public ReferenceLookupDelegate(
 	 *          {@literal null}.
 	 * @return can be {@literal null}.
 	 */
-	@Nullable
-	public Object readReference(MongoPersistentProperty property, Object source, LookupFunction lookupFunction,
+	public @Nullable Object readReference(MongoPersistentProperty property, Object source, LookupFunction lookupFunction,
 			MongoEntityReader entityReader) {
 
 		Object value = source instanceof DocumentReferenceSource documentReferenceSource
@@ -126,7 +125,7 @@ public Object readReference(MongoPersistentProperty property, Object source, Loo
 
 	@Nullable
 	private Iterable<Document> retrieveRawDocuments(MongoPersistentProperty property, Object source,
-			LookupFunction lookupFunction, Object value) {
+			LookupFunction lookupFunction, @Nullable Object value) {
 
 		DocumentReferenceQuery filter = computeFilter(property, source, spELContext);
 		if (filter instanceof NoResultsFilter) {
@@ -137,7 +136,8 @@ private Iterable<Document> retrieveRawDocuments(MongoPersistentProperty property
 		return lookupFunction.apply(filter, referenceCollection);
 	}
 
-	private ReferenceCollection computeReferenceContext(MongoPersistentProperty property, Object value,
+	@SuppressWarnings("NullAway")
+	private ReferenceCollection computeReferenceContext(MongoPersistentProperty property, @Nullable Object value,
 			SpELContext spELContext) {
 
 		// Use the first value as a reference for others in case of collection like
@@ -195,7 +195,7 @@ private ReferenceCollection computeReferenceContext(MongoPersistentProperty prop
 	 * @return can be {@literal null}.
 	 */
 	@SuppressWarnings("unchecked")
-	private <T> T parseValueOrGet(String value, ParameterBindingContext bindingContext, Supplier<T> defaultValue) {
+	private <T> T parseValueOrGet(String value, ParameterBindingContext bindingContext, Supplier<@Nullable T> defaultValue) {
 
 		if (!StringUtils.hasText(value)) {
 			return defaultValue.get();
@@ -220,7 +220,7 @@ private <T> T parseValueOrGet(String value, ParameterBindingContext bindingConte
 		return evaluated != null ? evaluated : defaultValue.get();
 	}
 
-	ParameterBindingContext bindingContext(MongoPersistentProperty property, Object source, SpELContext spELContext) {
+	ParameterBindingContext bindingContext(MongoPersistentProperty property, @Nullable Object source, SpELContext spELContext) {
 
 		ValueProvider valueProvider = valueProviderFor(DocumentReferenceSource.getTargetSource(source));
 
@@ -228,7 +228,7 @@ ParameterBindingContext bindingContext(MongoPersistentProperty property, Object
 				() -> evaluationContextFor(property, source, spELContext));
 	}
 
-	ValueProvider valueProviderFor(Object source) {
+	ValueProvider valueProviderFor(@Nullable Object source) {
 
 		return index -> {
 			if (source instanceof Document document) {
@@ -238,7 +238,7 @@ ValueProvider valueProviderFor(Object source) {
 		};
 	}
 
-	EvaluationContext evaluationContextFor(MongoPersistentProperty property, Object source, SpELContext spELContext) {
+	EvaluationContext evaluationContextFor(MongoPersistentProperty property, @Nullable Object source, SpELContext spELContext) {
 
 		Object target = source instanceof DocumentReferenceSource documentReferenceSource
 				? documentReferenceSource.getTargetSource()
@@ -264,7 +264,7 @@ EvaluationContext evaluationContextFor(MongoPersistentProperty property, Object
 	 * @param spELContext must not be {@literal null}.
 	 * @return never {@literal null}.
 	 */
-	@SuppressWarnings("unchecked")
+	@SuppressWarnings({"unchecked","NullAway"})
 	DocumentReferenceQuery computeFilter(MongoPersistentProperty property, Object source, SpELContext spELContext) {
 
 		DocumentReference documentReference = property.isDocumentReference() ? property.getDocumentReference()
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceResolver.java
index 715327d18e..0698b08bf8 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceResolver.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceResolver.java
@@ -15,11 +15,11 @@
  */
 package org.springframework.data.mongodb.core.convert;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mapping.PersistentProperty;
 import org.springframework.data.mongodb.MongoDatabaseFactory;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
 import org.springframework.data.util.TypeInformation;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
 import com.mongodb.DBRef;
@@ -54,8 +54,7 @@ Object resolveReference(MongoPersistentProperty property, Object source,
 	 */
 	class ReferenceCollection {
 
-		@Nullable //
-		private final String database;
+		private final @Nullable String database;
 		private final String collection;
 
 		/**
@@ -95,8 +94,7 @@ public String getCollection() {
 		 *
 		 * @return can be {@literal null}.
 		 */
-		@Nullable
-		public String getDatabase() {
+		public @Nullable String getDatabase() {
 			return database;
 		}
 	}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java
index 35cb578c23..bff72427f7 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java
@@ -23,18 +23,21 @@
 
 import org.bson.Document;
 import org.bson.conversions.Bson;
+import org.jspecify.annotations.Nullable;
 import org.springframework.core.convert.converter.Converter;
+import org.springframework.data.convert.PropertyValueConverter;
+import org.springframework.data.convert.ValueConversionContext;
 import org.springframework.data.domain.Sort;
 import org.springframework.data.domain.Sort.Order;
 import org.springframework.data.mapping.Association;
 import org.springframework.data.mapping.context.MappingContext;
+import org.springframework.data.mongodb.core.convert.MongoConversionContext.WriteOperatorContext;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mongodb.core.query.Update.Modifier;
 import org.springframework.data.mongodb.core.query.Update.Modifiers;
 import org.springframework.data.util.TypeInformation;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ObjectUtils;
 
 /**
@@ -129,7 +132,7 @@ public static boolean isUpdateObject(@Nullable Document updateObj) {
 	 *      org.springframework.data.mongodb.core.mapping.MongoPersistentEntity)
 	 */
 	@Override
-	protected Object delegateConvertToMongoType(Object source, @Nullable MongoPersistentEntity<?> entity) {
+	protected @Nullable Object delegateConvertToMongoType(Object source, @Nullable MongoPersistentEntity<?> entity) {
 
 		if (entity != null && entity.isUnwrapped()) {
 			return converter.convertToMongoType(source, entity);
@@ -140,7 +143,8 @@ protected Object delegateConvertToMongoType(Object source, @Nullable MongoPersis
 	}
 
 	@Override
-	protected Entry<String, Object> getMappedObjectForField(Field field, Object rawValue) {
+	@SuppressWarnings("NullAway")
+	protected Entry<String, @Nullable Object> getMappedObjectForField(Field field, @Nullable Object rawValue) {
 
 		if (isDocument(rawValue)) {
 
@@ -160,6 +164,13 @@ protected Entry<String, Object> getMappedObjectForField(Field field, Object rawV
 		return super.getMappedObjectForField(field, rawValue);
 	}
 
+	protected @Nullable Object convertValueWithConversionContext(Field documentField, @Nullable Object sourceValue, @Nullable Object value,
+		PropertyValueConverter<Object, Object, ValueConversionContext<MongoPersistentProperty>> valueConverter,
+		MongoConversionContext conversionContext) {
+
+		return super.convertValueWithConversionContext(documentField, sourceValue, value, valueConverter, conversionContext.forOperator(new WriteOperatorContext(documentField.name)));
+	}
+
 	private Entry<String, Object> getMappedUpdateModifier(Field field, Object rawValue) {
 		Object value;
 
@@ -196,11 +207,12 @@ private boolean isQuery(@Nullable Object value) {
 		return value instanceof Query;
 	}
 
-	private Document getMappedValue(@Nullable Field field, Modifier modifier) {
+	private @Nullable Document getMappedValue(@Nullable Field field, Modifier modifier) {
 		return new Document(modifier.getKey(), getMappedModifier(field, modifier));
 	}
 
-	private Object getMappedModifier(@Nullable Field field, Modifier modifier) {
+	@SuppressWarnings("NullAway")
+	private @Nullable Object getMappedModifier(@Nullable Field field, Modifier modifier) {
 
 		Object value = modifier.getValue();
 
@@ -211,7 +223,7 @@ private Object getMappedModifier(@Nullable Field field, Modifier modifier) {
 					: getMappedSort(sortObject, field.getPropertyEntity());
 		}
 
-		if (isAssociationConversionNecessary(field, value)) {
+		if (field != null && isAssociationConversionNecessary(field, value)) {
 			if (ObjectUtils.isArray(value) || value instanceof Collection) {
 				List<Object> targetPointers = new ArrayList<>();
 				for (Object val : converter.getConversionService().convert(value, List.class)) {
@@ -229,7 +241,7 @@ private Object getMappedModifier(@Nullable Field field, Modifier modifier) {
 	private TypeInformation<?> getTypeHintForEntity(@Nullable Object source, MongoPersistentEntity<?> entity) {
 
 		TypeInformation<?> info = entity.getTypeInformation();
-		Class<?> type = info.getActualType().getType();
+		Class<?> type = info.getRequiredActualType().getType();
 
 		if (source == null || type.isInterface() || java.lang.reflect.Modifier.isAbstract(type.getModifiers())) {
 			return info;
@@ -247,7 +259,7 @@ private TypeInformation<?> getTypeHintForEntity(@Nullable Object source, MongoPe
 	}
 
 	@Override
-	protected Field createPropertyField(MongoPersistentEntity<?> entity, String key,
+	protected Field createPropertyField(@Nullable MongoPersistentEntity<?> entity, String key,
 			MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
 
 		return entity == null ? super.createPropertyField(entity, key, mappingContext)
@@ -306,6 +318,7 @@ protected Converter<MongoPersistentProperty, String> getPropertyConverter() {
 		}
 
 		@Override
+		@SuppressWarnings("NullAway")
 		protected Converter<MongoPersistentProperty, String> getAssociationConverter() {
 			return new UpdateAssociationConverter(getMappingContext(), getAssociation(), key);
 		}
@@ -333,7 +346,7 @@ public UpdateAssociationConverter(
 			}
 
 			@Override
-			public String convert(MongoPersistentProperty source) {
+			public @Nullable String convert(MongoPersistentProperty source) {
 				return super.convert(source) == null ? null : mapper.mapPropertyName(source);
 			}
 		}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ValueResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ValueResolver.java
index 0a96cc867a..8eed053a09 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ValueResolver.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ValueResolver.java
@@ -17,10 +17,9 @@
 
 import org.bson.Document;
 import org.bson.conversions.Bson;
-
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mapping.model.ValueExpressionEvaluator;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
-import org.springframework.lang.Nullable;
 
 /**
  * Internal API to trigger the resolution of properties.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/EncryptingConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/EncryptingConverter.java
index 4097be7704..b31d8a2b7c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/EncryptingConverter.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/EncryptingConverter.java
@@ -15,6 +15,7 @@
  */
 package org.springframework.data.mongodb.core.convert.encryption;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.convert.MongoConversionContext;
 import org.springframework.data.mongodb.core.convert.MongoValueConverter;
 import org.springframework.data.mongodb.core.encryption.EncryptionContext;
@@ -28,7 +29,7 @@
 public interface EncryptingConverter<S, T> extends MongoValueConverter<S, T> {
 
 	@Override
-	default S read(Object value, MongoConversionContext context) {
+	default @Nullable S read(Object value, MongoConversionContext context) {
 		return decrypt(value, buildEncryptionContext(context));
 	}
 
@@ -39,10 +40,10 @@ default S read(Object value, MongoConversionContext context) {
 	 * @param context the context to operate in.
 	 * @return never {@literal null}.
 	 */
-	S decrypt(Object encryptedValue, EncryptionContext context);
+	@Nullable S decrypt(Object encryptedValue, EncryptionContext context);
 
 	@Override
-	default T write(Object value, MongoConversionContext context) {
+	default @Nullable T write(Object value, MongoConversionContext context) {
 		return encrypt(value, buildEncryptionContext(context));
 	}
 
@@ -53,7 +54,7 @@ default T write(Object value, MongoConversionContext context) {
 	 * @param context the context to operate in.
 	 * @return never {@literal null}.
 	 */
-	T encrypt(Object value, EncryptionContext context);
+	T encrypt(@Nullable Object value, EncryptionContext context);
 
 	/**
 	 * Obtain the {@link EncryptionContext} for a given {@link MongoConversionContext value conversion context}.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java
index f8d814fee4..0431cf11dd 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java
@@ -15,17 +15,19 @@
  */
 package org.springframework.data.mongodb.core.convert.encryption;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.convert.MongoConversionContext;
+import org.springframework.data.mongodb.core.convert.MongoConversionContext.OperatorContext;
 import org.springframework.data.mongodb.core.encryption.EncryptionContext;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
 import org.springframework.data.util.TypeInformation;
 import org.springframework.expression.EvaluationContext;
-import org.springframework.lang.Nullable;
 
 /**
  * Default {@link EncryptionContext} implementation.
  * 
  * @author Christoph Strobl
+ * @author Ross Lawley
  * @since 4.1
  */
 class ExplicitEncryptionContext implements EncryptionContext {
@@ -41,29 +43,39 @@ public MongoPersistentProperty getProperty() {
 		return conversionContext.getProperty();
 	}
 
-	@Nullable
 	@Override
-	public Object lookupValue(String path) {
+	public @Nullable Object lookupValue(String path) {
 		return conversionContext.getValue(path);
 	}
 
 	@Override
-	public Object convertToMongoType(Object value) {
+	public @Nullable Object convertToMongoType(Object value) {
 		return conversionContext.write(value);
 	}
 
 	@Override
-	public EvaluationContext getEvaluationContext(Object source) {
-		return conversionContext.getSpELContext().getEvaluationContext(source);
+	public EvaluationContext getEvaluationContext(@Nullable Object source) {
+
+		if(conversionContext.getSpELContext() != null) {
+			return conversionContext.getSpELContext().getEvaluationContext(source);
+		}
+
+		throw new IllegalStateException("SpEL context not present");
 	}
 
 	@Override
-	public <T> T read(@Nullable Object value, TypeInformation<T> target) {
+	public <T> @Nullable T read(@Nullable Object value, TypeInformation<T> target) {
 		return conversionContext.read(value, target);
 	}
 
 	@Override
-	public <T> T write(@Nullable Object value, TypeInformation<T> target) {
+	public <T> @Nullable T write(@Nullable Object value, TypeInformation<T> target) {
 		return conversionContext.write(value, target);
 	}
+
+	@Override
+	@Nullable
+	public OperatorContext getOperatorContext() {
+		return conversionContext.getOperatorContext();
+	}
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java
index 1ce24b25fe..ecab645fe5 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java
@@ -15,8 +15,13 @@
  */
 package org.springframework.data.mongodb.core.convert.encryption;
 
+import static java.util.Arrays.*;
+import static java.util.Collections.*;
+import static org.springframework.data.mongodb.core.encryption.EncryptionOptions.*;
+
 import java.util.Collection;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 
 import org.apache.commons.logging.Log;
@@ -27,28 +32,36 @@
 import org.bson.BsonValue;
 import org.bson.Document;
 import org.bson.types.Binary;
+
+import org.jspecify.annotations.Nullable;
 import org.springframework.core.CollectionFactory;
 import org.springframework.data.mongodb.core.convert.MongoConversionContext;
+import org.springframework.data.mongodb.core.convert.MongoConversionContext.OperatorContext;
 import org.springframework.data.mongodb.core.encryption.Encryption;
 import org.springframework.data.mongodb.core.encryption.EncryptionContext;
+import org.springframework.data.mongodb.core.encryption.EncryptionKey;
 import org.springframework.data.mongodb.core.encryption.EncryptionKeyResolver;
 import org.springframework.data.mongodb.core.encryption.EncryptionOptions;
 import org.springframework.data.mongodb.core.mapping.Encrypted;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
+import org.springframework.data.mongodb.core.mapping.Queryable;
 import org.springframework.data.mongodb.util.BsonUtils;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
 
 /**
  * Default implementation of {@link EncryptingConverter}. Properties used with this converter must be annotated with
  * {@link Encrypted @Encrypted} to provide key and algorithm metadata.
  *
  * @author Christoph Strobl
+ * @author Ross Lawley
  * @since 4.1
  */
 public class MongoEncryptionConverter implements EncryptingConverter<Object, Object> {
 
 	private static final Log LOGGER = LogFactory.getLog(MongoEncryptionConverter.class);
+	private static final List<String> RANGE_OPERATORS = asList("$gt", "$gte", "$lt", "$lte");
+	public static final String AND_OPERATOR = "$and";
 
 	private final Encryption<BsonValue, BsonBinary> encryption;
 	private final EncryptionKeyResolver keyResolver;
@@ -59,16 +72,15 @@ public MongoEncryptionConverter(Encryption<BsonValue, BsonBinary> encryption, En
 		this.keyResolver = keyResolver;
 	}
 
-	@Nullable
 	@Override
-	public Object read(Object value, MongoConversionContext context) {
+	public @Nullable Object read(Object value, MongoConversionContext context) {
 
 		Object decrypted = EncryptingConverter.super.read(value, context);
 		return decrypted instanceof BsonValue bsonValue ? BsonUtils.toJavaType(bsonValue) : decrypted;
 	}
 
 	@Override
-	public Object decrypt(Object encryptedValue, EncryptionContext context) {
+	public @Nullable Object decrypt(Object encryptedValue, EncryptionContext context) {
 
 		Object decryptedValue = encryptedValue;
 		if (encryptedValue instanceof Binary || encryptedValue instanceof BsonBinary) {
@@ -142,7 +154,8 @@ public Object decrypt(Object encryptedValue, EncryptionContext context) {
 	}
 
 	@Override
-	public Object encrypt(Object value, EncryptionContext context) {
+	@SuppressWarnings("NullAway")
+	public Object encrypt(@Nullable Object value, EncryptionContext context) {
 
 		if (LOGGER.isDebugEnabled()) {
 			LOGGER.debug(String.format("Encrypting %s.%s.", getProperty(context).getOwner().getName(),
@@ -158,10 +171,52 @@ public Object encrypt(Object value, EncryptionContext context) {
 
 		if (annotation == null) {
 			throw new IllegalStateException(String.format("Property %s.%s is not annotated with @Encrypted",
-					getProperty(context).getOwner().getName(), getProperty(context).getName()));
+					persistentProperty.getOwner().getName(), persistentProperty.getName()));
+		}
+
+		String algorithm = annotation.algorithm();
+		EncryptionKey key = keyResolver.getKey(context);
+		OperatorContext operatorContext = context.getOperatorContext();
+
+		EncryptionOptions encryptionOptions = new EncryptionOptions(algorithm, key,
+				getEQOptions(persistentProperty, operatorContext));
+
+		if (operatorContext != null && !operatorContext.isWriteOperation() && encryptionOptions.queryableEncryptionOptions() != null
+				&& !encryptionOptions.queryableEncryptionOptions().getQueryType().equals("equality")) {
+			return encryptExpression(operatorContext, value, encryptionOptions);
+		} else {
+			return encryptValue(value, context, persistentProperty, encryptionOptions);
+		}
+	}
+
+	private static @Nullable QueryableEncryptionOptions getEQOptions(MongoPersistentProperty persistentProperty,
+			@Nullable OperatorContext operatorContext) {
+
+		Queryable queryableAnnotation = persistentProperty.findAnnotation(Queryable.class);
+		if (queryableAnnotation == null || !StringUtils.hasText(queryableAnnotation.queryType())) {
+			return null;
+		}
+
+		QueryableEncryptionOptions queryableEncryptionOptions = QueryableEncryptionOptions.none();
+
+		String queryAttributes = queryableAnnotation.queryAttributes();
+		if (!queryAttributes.isEmpty()) {
+			queryableEncryptionOptions = queryableEncryptionOptions.attributes(Document.parse(queryAttributes));
+		}
+
+		if (queryableAnnotation.contentionFactor() >= 0) {
+			queryableEncryptionOptions = queryableEncryptionOptions.contentionFactor(queryableAnnotation.contentionFactor());
 		}
 
-		EncryptionOptions encryptionOptions = new EncryptionOptions(annotation.algorithm(), keyResolver.getKey(context));
+		boolean isPartOfARangeQuery = operatorContext != null && !operatorContext.isWriteOperation();
+		if (isPartOfARangeQuery) {
+			queryableEncryptionOptions = queryableEncryptionOptions.queryType(queryableAnnotation.queryType());
+		}
+		return queryableEncryptionOptions;
+	}
+
+	private BsonBinary encryptValue(Object value, EncryptionContext context, MongoPersistentProperty persistentProperty,
+			EncryptionOptions encryptionOptions) {
 
 		if (!persistentProperty.isEntity()) {
 
@@ -176,6 +231,7 @@ public Object encrypt(Object value, EncryptionContext context) {
 			}
 			return encryption.encrypt(BsonUtils.simpleToBsonValue(value), encryptionOptions);
 		}
+
 		if (persistentProperty.isCollectionLike()) {
 			return encryption.encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptionOptions);
 		}
@@ -187,6 +243,37 @@ public Object encrypt(Object value, EncryptionContext context) {
 		return encryption.encrypt(BsonUtils.simpleToBsonValue(write), encryptionOptions);
 	}
 
+	/**
+	 * Encrypts a range query expression.
+	 * <p>
+	 * The mongodb-crypt {@code encryptExpression} has strict formatting requirements so this method ensures these
+	 * requirements are met and then picks out and returns just the value for use with a range query.
+	 *
+	 * @param operatorContext field name and query operator.
+	 * @param value the value of the expression to be encrypted.
+	 * @param encryptionOptions the options.
+	 * @return the encrypted range value for use in a range query.
+	 */
+	private BsonValue encryptExpression(OperatorContext operatorContext, Object value,
+			EncryptionOptions encryptionOptions) {
+
+		BsonValue doc = BsonUtils.simpleToBsonValue(value);
+
+		String fieldName = operatorContext.path();
+		String queryOperator = operatorContext.operator();
+
+		if (!RANGE_OPERATORS.contains(queryOperator)) {
+			throw new AssertionError(String.format("Not a valid range query. Querying a range encrypted field but the "
+					+ "query operator '%s' for field path '%s' is not a range query.", queryOperator, fieldName));
+		}
+
+		BsonDocument encryptExpression = new BsonDocument(AND_OPERATOR,
+				new BsonArray(singletonList(new BsonDocument(fieldName, new BsonDocument(queryOperator, doc)))));
+
+		BsonDocument result = encryption.encryptExpression(encryptExpression, encryptionOptions);
+		return result.getArray(AND_OPERATOR).get(0).asDocument().getDocument(fieldName).getBinary(queryOperator);
+	}
+
 	private BsonValue collectionLikeToBsonValue(Object value, MongoPersistentProperty property,
 			EncryptionContext context) {
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/package-info.java
index 4a6f78357a..a0e8ea27f7 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/package-info.java
@@ -3,5 +3,5 @@
  * <a href="https://www.mongodb.com/docs/manual/core/csfle/fundamentals/manual-encryption/">explicit encryption
  * mechanism of Client-Side Field Level Encryption</a>.
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 package org.springframework.data.mongodb.core.convert.encryption;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/package-info.java
index cfa07fa8f9..dbef5cbb90 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/package-info.java
@@ -1,6 +1,6 @@
 /**
  * Spring Data MongoDB specific converter infrastructure.
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 package org.springframework.data.mongodb.core.convert;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java
index 5645c1e416..a80a72ed1f 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java
@@ -15,13 +15,18 @@
  */
 package org.springframework.data.mongodb.core.encryption;
 
+import org.bson.BsonDocument;
+
 /**
  * Component responsible for encrypting and decrypting values.
  *
+ * @param <P> plaintext type.
+ * @param <C> ciphertext type.
  * @author Christoph Strobl
+ * @author Ross Lawley
  * @since 4.1
  */
-public interface Encryption<S, T> {
+public interface Encryption<P, C> {
 
 	/**
 	 * Encrypt the given value.
@@ -30,7 +35,7 @@ public interface Encryption<S, T> {
 	 * @param options must not be {@literal null}.
 	 * @return the encrypted value.
 	 */
-	T encrypt(S value, EncryptionOptions options);
+	C encrypt(P value, EncryptionOptions options);
 
 	/**
 	 * Decrypt the given value.
@@ -38,6 +43,18 @@ public interface Encryption<S, T> {
 	 * @param value must not be {@literal null}.
 	 * @return the decrypted value.
 	 */
-	S decrypt(T value);
+	P decrypt(C value);
+
+	/**
+	 * Encrypt the given expression.
+	 *
+	 * @param value must not be {@literal null}.
+	 * @param options must not be {@literal null}.
+	 * @return the encrypted expression.
+	 * @since 4.5.0
+	 */
+	default BsonDocument encryptExpression(BsonDocument value, EncryptionOptions options) {
+		throw new UnsupportedOperationException("Unsupported encryption method");
+	}
 
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java
index 89beaadedb..45e83ed7ca 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java
@@ -15,16 +15,18 @@
  */
 package org.springframework.data.mongodb.core.encryption;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mapping.PersistentProperty;
+import org.springframework.data.mongodb.core.convert.MongoConversionContext.OperatorContext;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
 import org.springframework.data.util.TypeInformation;
 import org.springframework.expression.EvaluationContext;
-import org.springframework.lang.Nullable;
 
 /**
  * Context to encapsulate encryption for a specific {@link MongoPersistentProperty}.
  *
  * @author Christoph Strobl
+ * @author Ross Lawley
  * @since 4.1
  */
 public interface EncryptionContext {
@@ -43,7 +45,7 @@ public interface EncryptionContext {
 	 * @param value
 	 * @return
 	 */
-	Object convertToMongoType(Object value);
+	@Nullable Object convertToMongoType(Object value);
 
 	/**
 	 * Reads the value as an instance of the {@link PersistentProperty#getTypeInformation() property type}.
@@ -52,7 +54,7 @@ public interface EncryptionContext {
 	 * @return can be {@literal null}.
 	 * @throws IllegalStateException if value cannot be read as an instance of {@link Class type}.
 	 */
-	default <T> T read(@Nullable Object value) {
+	default <T> @Nullable T read(@Nullable Object value) {
 		return (T) read(value, getProperty().getTypeInformation());
 	}
 
@@ -64,7 +66,7 @@ default <T> T read(@Nullable Object value) {
 	 * @return can be {@literal null}.
 	 * @throws IllegalStateException if value cannot be read as an instance of {@link Class type}.
 	 */
-	default <T> T read(@Nullable Object value, Class<T> target) {
+	default <T> @Nullable T read(@Nullable Object value, Class<T> target) {
 		return read(value, TypeInformation.of(target));
 	}
 
@@ -76,7 +78,7 @@ default <T> T read(@Nullable Object value, Class<T> target) {
 	 * @return can be {@literal null}.
 	 * @throws IllegalStateException if value cannot be read as an instance of {@link Class type}.
 	 */
-	<T> T read(@Nullable Object value, TypeInformation<T> target);
+	<T> @Nullable T read(@Nullable Object value, TypeInformation<T> target);
 
 	/**
 	 * Write the value as an instance of the {@link PersistentProperty#getTypeInformation() property type}.
@@ -88,8 +90,7 @@ default <T> T read(@Nullable Object value, Class<T> target) {
 	 * @see PersistentProperty#getTypeInformation()
 	 * @see #write(Object, TypeInformation)
 	 */
-	@Nullable
-	default <T> T write(@Nullable Object value) {
+	default <T> @Nullable T write(@Nullable Object value) {
 		return (T) write(value, getProperty().getTypeInformation());
 	}
 
@@ -101,8 +102,7 @@ default <T> T write(@Nullable Object value) {
 	 * @return can be {@literal null}.
 	 * @throws IllegalStateException if value cannot be written as an instance of {@link Class type}.
 	 */
-	@Nullable
-	default <T> T write(@Nullable Object value, Class<T> target) {
+	default <T> @Nullable T write(@Nullable Object value, Class<T> target) {
 		return write(value, TypeInformation.of(target));
 	}
 
@@ -114,8 +114,7 @@ default <T> T write(@Nullable Object value, Class<T> target) {
 	 * @return can be {@literal null}.
 	 * @throws IllegalStateException if value cannot be written as an instance of {@link Class type}.
 	 */
-	@Nullable
-	<T> T write(@Nullable Object value, TypeInformation<T> target);
+	<T> @Nullable T write(@Nullable Object value, TypeInformation<T> target);
 
 	/**
 	 * Lookup the value for a given path within the current context.
@@ -126,6 +125,15 @@ default <T> T write(@Nullable Object value, Class<T> target) {
 	@Nullable
 	Object lookupValue(String path);
 
-	EvaluationContext getEvaluationContext(Object source);
+	EvaluationContext getEvaluationContext(@Nullable Object source);
 
+	/**
+	 * The field name and field query operator
+	 *
+	 * @return can be {@literal null}.
+	 */
+	@Nullable
+	default OperatorContext getOperatorContext() {
+		return null;
+	}
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java
index fe01cfa8ba..7a3e8a2c76 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java
@@ -15,27 +15,41 @@
  */
 package org.springframework.data.mongodb.core.encryption;
 
+import java.util.Map;
+import java.util.Objects;
+
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 
 /**
- * Options, like the {@link #algorithm()}, to apply when encrypting values.
- *
+ * Options used to provide additional information when {@link Encryption encrypting} values. like the
+ * {@link #algorithm()} to be used.
+ * 
  * @author Christoph Strobl
+ * @author Ross Lawley
  * @since 4.1
  */
 public class EncryptionOptions {
 
 	private final String algorithm;
 	private final EncryptionKey key;
+	private final @Nullable QueryableEncryptionOptions queryableEncryptionOptions;
 
 	public EncryptionOptions(String algorithm, EncryptionKey key) {
+		this(algorithm, key, null);
+	}
+
+	public EncryptionOptions(String algorithm, EncryptionKey key,
+			@Nullable QueryableEncryptionOptions queryableEncryptionOptions) {
 
 		Assert.hasText(algorithm, "Algorithm must not be empty");
 		Assert.notNull(key, "EncryptionKey must not be empty");
+		Assert.notNull(key, "QueryableEncryptionOptions must not be empty");
 
 		this.key = key;
 		this.algorithm = algorithm;
+		this.queryableEncryptionOptions = queryableEncryptionOptions;
 	}
 
 	public EncryptionKey key() {
@@ -46,6 +60,14 @@ public String algorithm() {
 		return algorithm;
 	}
 
+	/**
+	 * @return {@literal null} if not set.
+	 * @since 4.5
+	 */
+	public @Nullable QueryableEncryptionOptions queryableEncryptionOptions() {
+		return queryableEncryptionOptions;
+	}
+
 	@Override
 	public boolean equals(Object o) {
 
@@ -61,7 +83,11 @@ public boolean equals(Object o) {
 		if (!ObjectUtils.nullSafeEquals(algorithm, that.algorithm)) {
 			return false;
 		}
-		return ObjectUtils.nullSafeEquals(key, that.key);
+		if (!ObjectUtils.nullSafeEquals(key, that.key)) {
+			return false;
+		}
+
+		return ObjectUtils.nullSafeEquals(queryableEncryptionOptions, that.queryableEncryptionOptions);
 	}
 
 	@Override
@@ -69,11 +95,141 @@ public int hashCode() {
 
 		int result = ObjectUtils.nullSafeHashCode(algorithm);
 		result = 31 * result + ObjectUtils.nullSafeHashCode(key);
+		result = 31 * result + ObjectUtils.nullSafeHashCode(queryableEncryptionOptions);
 		return result;
 	}
 
 	@Override
 	public String toString() {
-		return "EncryptionOptions{" + "algorithm='" + algorithm + '\'' + ", key=" + key + '}';
+		return "EncryptionOptions{" + "algorithm='" + algorithm + '\'' + ", key=" + key + ", queryableEncryptionOptions='"
+				+ queryableEncryptionOptions + "'}";
+	}
+
+	/**
+	 * Options, like the {@link #getQueryType()}, to apply when encrypting queryable values.
+	 *
+	 * @author Ross Lawley
+	 * @author Christoph Strobl
+	 * @since 4.5
+	 */
+	public static class QueryableEncryptionOptions {
+
+		private static final QueryableEncryptionOptions NONE = new QueryableEncryptionOptions(null, null, Map.of());
+
+		private final @Nullable String queryType;
+		private final @Nullable Long contentionFactor;
+		private final Map<String, Object> attributes;
+
+		private QueryableEncryptionOptions(@Nullable String queryType, @Nullable Long contentionFactor,
+				Map<String, Object> attributes) {
+
+			this.queryType = queryType;
+			this.contentionFactor = contentionFactor;
+			this.attributes = attributes;
+		}
+
+		/**
+		 * Create an empty {@link QueryableEncryptionOptions}.
+		 *
+		 * @return unmodifiable {@link QueryableEncryptionOptions} instance.
+		 */
+		public static QueryableEncryptionOptions none() {
+			return NONE;
+		}
+
+		/**
+		 * Define the {@code queryType} to be used for queryable document encryption.
+		 *
+		 * @param queryType can be {@literal null}.
+		 * @return new instance of {@link QueryableEncryptionOptions}.
+		 */
+		public QueryableEncryptionOptions queryType(@Nullable String queryType) {
+			return new QueryableEncryptionOptions(queryType, contentionFactor, attributes);
+		}
+
+		/**
+		 * Define the {@code contentionFactor} to be used for queryable document encryption.
+		 *
+		 * @param contentionFactor can be {@literal null}.
+		 * @return new instance of {@link QueryableEncryptionOptions}.
+		 */
+		public QueryableEncryptionOptions contentionFactor(@Nullable Long contentionFactor) {
+			return new QueryableEncryptionOptions(queryType, contentionFactor, attributes);
+		}
+
+		/**
+		 * Define the {@code rangeOptions} to be used for queryable document encryption.
+		 *
+		 * @param attributes can be {@literal null}.
+		 * @return new instance of {@link QueryableEncryptionOptions}.
+		 */
+		public QueryableEncryptionOptions attributes(Map<String, Object> attributes) {
+			return new QueryableEncryptionOptions(queryType, contentionFactor, attributes);
+		}
+
+		/**
+		 * Get the {@code queryType} to apply.
+		 *
+		 * @return {@literal null} if not set.
+		 */
+		public @Nullable String getQueryType() {
+			return queryType;
+		}
+
+		/**
+		 * Get the {@code contentionFactor} to apply.
+		 *
+		 * @return {@literal null} if not set.
+		 */
+		public @Nullable Long getContentionFactor() {
+			return contentionFactor;
+		}
+
+		/**
+		 * Get the {@code rangeOptions} to apply.
+		 *
+		 * @return never {@literal null}.
+		 */
+		public Map<String, Object> getAttributes() {
+			return Map.copyOf(attributes);
+		}
+
+		/**
+		 * @return {@literal true} if no arguments set.
+		 */
+		boolean isEmpty() {
+			return getQueryType() == null && getContentionFactor() == null && getAttributes().isEmpty();
+		}
+
+		@Override
+		public String toString() {
+			return "QueryableEncryptionOptions{" + "queryType='" + queryType + '\'' + ", contentionFactor=" + contentionFactor
+					+ ", attributes=" + attributes + '}';
+		}
+
+		@Override
+		public boolean equals(Object o) {
+			if (this == o) {
+				return true;
+			}
+			if (o == null || getClass() != o.getClass()) {
+				return false;
+			}
+			QueryableEncryptionOptions that = (QueryableEncryptionOptions) o;
+
+			if (!ObjectUtils.nullSafeEquals(queryType, that.queryType)) {
+				return false;
+			}
+
+			if (!ObjectUtils.nullSafeEquals(contentionFactor, that.contentionFactor)) {
+				return false;
+			}
+			return ObjectUtils.nullSafeEquals(attributes, that.attributes);
+		}
+
+		@Override
+		public int hashCode() {
+			return Objects.hash(queryType, contentionFactor, attributes);
+		}
 	}
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java
index 92350ce7d7..aee5dd7f8b 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java
@@ -15,20 +15,26 @@
  */
 package org.springframework.data.mongodb.core.encryption;
 
+import java.util.Map;
 import java.util.function.Supplier;
 
 import org.bson.BsonBinary;
+import org.bson.BsonDocument;
 import org.bson.BsonValue;
 import org.springframework.data.mongodb.core.encryption.EncryptionKey.Type;
+import org.springframework.data.mongodb.core.encryption.EncryptionOptions.QueryableEncryptionOptions;
+import org.springframework.data.mongodb.util.BsonUtils;
 import org.springframework.util.Assert;
 
 import com.mongodb.client.model.vault.EncryptOptions;
+import com.mongodb.client.model.vault.RangeOptions;
 import com.mongodb.client.vault.ClientEncryption;
 
 /**
  * {@link ClientEncryption} based {@link Encryption} implementation.
  *
  * @author Christoph Strobl
+ * @author Ross Lawley
  * @since 4.1
  */
 public class MongoClientEncryption implements Encryption<BsonValue, BsonBinary> {
@@ -59,6 +65,19 @@ public BsonValue decrypt(BsonBinary value) {
 
 	@Override
 	public BsonBinary encrypt(BsonValue value, EncryptionOptions options) {
+		return getClientEncryption().encrypt(value, createEncryptOptions(options));
+	}
+
+	@Override
+	public BsonDocument encryptExpression(BsonDocument value, EncryptionOptions options) {
+		return getClientEncryption().encryptExpression(value, createEncryptOptions(options));
+	}
+
+	public ClientEncryption getClientEncryption() {
+		return source.get();
+	}
+
+	private EncryptOptions createEncryptOptions(EncryptionOptions options) {
 
 		EncryptOptions encryptOptions = new EncryptOptions(options.algorithm());
 
@@ -68,11 +87,58 @@ public BsonBinary encrypt(BsonValue value, EncryptionOptions options) {
 			encryptOptions = encryptOptions.keyId((BsonBinary) options.key().value());
 		}
 
-		return getClientEncryption().encrypt(value, encryptOptions);
+		if (options.queryableEncryptionOptions() == null) {
+			return encryptOptions;
+		}
+
+		QueryableEncryptionOptions qeOptions = options.queryableEncryptionOptions();
+		if (qeOptions.getQueryType() != null) {
+			encryptOptions.queryType(qeOptions.getQueryType());
+		}
+		if (qeOptions.getContentionFactor() != null) {
+			encryptOptions.contentionFactor(qeOptions.getContentionFactor());
+		}
+		if (!qeOptions.getAttributes().isEmpty()) {
+			encryptOptions.rangeOptions(rangeOptions(qeOptions.getAttributes()));
+		}
+		return encryptOptions;
 	}
 
-	public ClientEncryption getClientEncryption() {
-		return source.get();
+	protected RangeOptions rangeOptions(Map<String, Object> attributes) {
+
+		RangeOptions encryptionRangeOptions = new RangeOptions();
+		if (attributes.isEmpty()) {
+			return encryptionRangeOptions;
+		}
+
+		if (attributes.containsKey("min")) {
+			encryptionRangeOptions.min(BsonUtils.simpleToBsonValue(attributes.get("min")));
+		}
+		if (attributes.containsKey("max")) {
+			encryptionRangeOptions.max(BsonUtils.simpleToBsonValue(attributes.get("max")));
+		}
+		if (attributes.containsKey("trimFactor")) {
+			Object trimFactor = attributes.get("trimFactor");
+			Assert.isInstanceOf(Integer.class, trimFactor, () -> String
+					.format("Expected to find a %s but it turned out to be %s.", Integer.class, trimFactor.getClass()));
+
+			encryptionRangeOptions.trimFactor((Integer) trimFactor);
+		}
+
+		if (attributes.containsKey("sparsity")) {
+			Object sparsity = attributes.get("sparsity");
+			Assert.isInstanceOf(Number.class, sparsity,
+					() -> String.format("Expected to find a %s but it turned out to be %s.", Long.class, sparsity.getClass()));
+			encryptionRangeOptions.sparsity(((Number) sparsity).longValue());
+		}
+
+		if (attributes.containsKey("precision")) {
+			Object precision = attributes.get("precision");
+			Assert.isInstanceOf(Number.class, precision, () -> String
+					.format("Expected to find a %s but it turned out to be %s.", Integer.class, precision.getClass()));
+			encryptionRangeOptions.precision(((Number) precision).intValue());
+		}
+		return encryptionRangeOptions;
 	}
 
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/package-info.java
index f3906d89dd..90a3ab8720 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/package-info.java
@@ -2,5 +2,5 @@
  * Infrastructure for <a href="https://www.mongodb.com/docs/manual/core/csfle/fundamentals/manual-encryption/">explicit
  * encryption mechanism of Client-Side Field Level Encryption</a>.
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 package org.springframework.data.mongodb.core.encryption;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonGeometryCollection.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonGeometryCollection.java
index 2372700aec..74f36e3198 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonGeometryCollection.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonGeometryCollection.java
@@ -19,7 +19,7 @@
 import java.util.Collections;
 import java.util.List;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonModule.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonModule.java
index bc74a56df3..5c458329ab 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonModule.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonModule.java
@@ -20,8 +20,9 @@
 import java.util.Collections;
 import java.util.List;
 
+import org.jspecify.annotations.Nullable;
+
 import org.springframework.data.geo.Point;
-import org.springframework.lang.Nullable;
 
 import com.fasterxml.jackson.core.JsonParser;
 import com.fasterxml.jackson.core.Version;
@@ -139,9 +140,8 @@ private static void registerDeserializersIn(SimpleModule module) {
 	 */
 	private static abstract class GeoJsonDeserializer<T extends GeoJson<?>> extends JsonDeserializer<T> {
 
-		@Nullable
 		@Override
-		public T deserialize(@Nullable JsonParser jp, @Nullable DeserializationContext ctxt) throws IOException {
+		public @Nullable T deserialize(JsonParser jp, @Nullable DeserializationContext ctxt) throws IOException {
 
 			JsonNode node = jp.readValueAsTree();
 			JsonNode coordinates = node.get("coordinates");
@@ -158,18 +158,16 @@ public T deserialize(@Nullable JsonParser jp, @Nullable DeserializationContext c
 		 * @param coordinates
 		 * @return
 		 */
-		@Nullable
-		protected abstract T doDeserialize(ArrayNode coordinates);
+		protected abstract @Nullable T doDeserialize(ArrayNode coordinates);
 
 		/**
 		 * Get the {@link GeoJsonPoint} representation of given {@link ArrayNode} assuming {@code node.[0]} represents
 		 * {@literal x - coordinate} and {@code node.[1]} is {@literal y}.
 		 *
 		 * @param node can be {@literal null}.
-		 * @return {@literal null} when given a {@code null} value.
+		 * @return {@literal null} when given a {@literal null} value.
 		 */
-		@Nullable
-		protected GeoJsonPoint toGeoJsonPoint(@Nullable ArrayNode node) {
+		protected @Nullable GeoJsonPoint toGeoJsonPoint(@Nullable ArrayNode node) {
 
 			if (node == null) {
 				return null;
@@ -183,10 +181,9 @@ protected GeoJsonPoint toGeoJsonPoint(@Nullable ArrayNode node) {
 		 * {@literal x - coordinate} and {@code node.[1]} is {@literal y}.
 		 *
 		 * @param node can be {@literal null}.
-		 * @return {@literal null} when given a {@code null} value.
+		 * @return {@literal null} when given a {@literal null} value.
 		 */
-		@Nullable
-		protected Point toPoint(@Nullable ArrayNode node) {
+		protected @Nullable Point toPoint(@Nullable ArrayNode node) {
 
 			if (node == null) {
 				return null;
@@ -199,7 +196,7 @@ protected Point toPoint(@Nullable ArrayNode node) {
 		 * Get the points nested within given {@link ArrayNode}.
 		 *
 		 * @param node can be {@literal null}.
-		 * @return {@literal empty list} when given a {@code null} value.
+		 * @return {@literal empty list} when given a {@literal null} value.
 		 */
 		protected List<Point> toPoints(@Nullable ArrayNode node) {
 
@@ -236,9 +233,8 @@ protected GeoJsonLineString toLineString(ArrayNode node) {
 	 */
 	private static class GeoJsonPointDeserializer extends GeoJsonDeserializer<GeoJsonPoint> {
 
-		@Nullable
 		@Override
-		protected GeoJsonPoint doDeserialize(ArrayNode coordinates) {
+		protected @Nullable GeoJsonPoint doDeserialize(ArrayNode coordinates) {
 			return toGeoJsonPoint(coordinates);
 		}
 	}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiLineString.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiLineString.java
index 8dafe9ea00..833a1dd9f6 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiLineString.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiLineString.java
@@ -19,8 +19,8 @@
 import java.util.Collections;
 import java.util.List;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.geo.Point;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPoint.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPoint.java
index bcb4c3e79e..e30ed5d6ed 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPoint.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPoint.java
@@ -20,8 +20,8 @@
 import java.util.Collections;
 import java.util.List;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.geo.Point;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPolygon.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPolygon.java
index 12b9de9da4..a7e6306b49 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPolygon.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPolygon.java
@@ -19,7 +19,7 @@
 import java.util.Collections;
 import java.util.List;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonPolygon.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonPolygon.java
index 166a10df08..990be290cd 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonPolygon.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonPolygon.java
@@ -21,9 +21,10 @@
 import java.util.Iterator;
 import java.util.List;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.geo.Point;
 import org.springframework.data.geo.Polygon;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 
@@ -78,6 +79,7 @@ public GeoJsonPolygon(List<Point> points) {
 	 * @return new {@link GeoJsonPolygon}.
 	 * @since 1.10
 	 */
+	@Contract("_, _, _, _, _ -> new")
 	public GeoJsonPolygon withInnerRing(Point first, Point second, Point third, Point fourth, Point... others) {
 		return withInnerRing(asList(first, second, third, fourth, others));
 	}
@@ -88,6 +90,7 @@ public GeoJsonPolygon withInnerRing(Point first, Point second, Point third, Poin
 	 * @param points must not be {@literal null}.
 	 * @return new {@link GeoJsonPolygon}.
 	 */
+	@Contract("_ -> new")
 	public GeoJsonPolygon withInnerRing(List<Point> points) {
 		return withInnerRing(new GeoJsonLineString(points));
 	}
@@ -99,6 +102,7 @@ public GeoJsonPolygon withInnerRing(List<Point> points) {
 	 * @return new {@link GeoJsonPolygon}.
 	 * @since 1.10
 	 */
+	@Contract("_ -> new")
 	public GeoJsonPolygon withInnerRing(GeoJsonLineString lineString) {
 
 		Assert.notNull(lineString, "LineString must not be null");
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Sphere.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Sphere.java
index a482c136e7..d3ca840d6b 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Sphere.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Sphere.java
@@ -18,12 +18,12 @@
 import java.util.Arrays;
 import java.util.List;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.annotation.PersistenceCreator;
 import org.springframework.data.geo.Circle;
 import org.springframework.data.geo.Distance;
 import org.springframework.data.geo.Point;
 import org.springframework.data.geo.Shape;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
 /**
@@ -63,7 +63,7 @@ public Sphere(Point center, Distance radius) {
 	 * @param radius
 	 */
 	public Sphere(Point center, double radius) {
-		this(center, new Distance(radius));
+		this(center, Distance.of(radius));
 	}
 
 	/**
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/package-info.java
index 6cc77f832b..e5adfb26f4 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/package-info.java
@@ -1,6 +1,6 @@
 /**
  * Support for MongoDB geo-spatial queries.
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 package org.springframework.data.mongodb.core.geo;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/DefaultSearchIndexOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/DefaultSearchIndexOperations.java
index 225bb41ac8..b4b7b8430a 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/DefaultSearchIndexOperations.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/DefaultSearchIndexOperations.java
@@ -20,12 +20,12 @@
 
 import org.bson.BsonString;
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mapping.context.MappingContext;
 import org.springframework.data.mongodb.core.MongoOperations;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
 import org.springframework.data.util.TypeInformation;
-import org.springframework.lang.Nullable;
 import org.springframework.util.StringUtils;
 
 import com.mongodb.client.model.SearchIndexModel;
@@ -40,7 +40,7 @@ public class DefaultSearchIndexOperations implements SearchIndexOperations {
 
 	private final MongoOperations mongoOperations;
 	private final String collectionName;
-	private final TypeInformation<?> entityTypeInformation;
+	private final @Nullable TypeInformation<?> entityTypeInformation;
 
 	public DefaultSearchIndexOperations(MongoOperations mongoOperations, Class<?> type) {
 		this(mongoOperations, mongoOperations.getCollectionName(type), type);
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeoSpatialIndexed.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeoSpatialIndexed.java
index 3fb797559b..a39da5c946 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeoSpatialIndexed.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeoSpatialIndexed.java
@@ -114,16 +114,6 @@
 	 */
 	GeoSpatialIndexType type() default GeoSpatialIndexType.GEO_2D;
 
-	/**
-	 * The bucket size for {@link GeoSpatialIndexType#GEO_HAYSTACK} indexes, in coordinate units.
-	 *
-	 * @since 1.4
-	 * @return {@literal 1.0} by default.
-	 * @deprecated since MongoDB server version 4.4
-	 */
-	@Deprecated
-	double bucketSize() default 1.0;
-
 	/**
 	 * The name of the additional field to use for {@link GeoSpatialIndexType#GEO_HAYSTACK} indexes
 	 *
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java
index 0949506195..c1ce25776b 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java
@@ -18,9 +18,9 @@
 import java.util.Optional;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.query.Collation;
-import org.springframework.data.mongodb.util.MongoClientVersion;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 
@@ -41,7 +41,6 @@ public class GeospatialIndex implements IndexDefinition {
 	private @Nullable Integer max;
 	private @Nullable Integer bits;
 	private GeoSpatialIndexType type = GeoSpatialIndexType.GEO_2D;
-	private Double bucketSize = MongoClientVersion.isVersion5orNewer() ? null : 1.0;
 	private @Nullable String additionalField;
 	private Optional<IndexFilter> filter = Optional.empty();
 	private Optional<Collation> collation = Optional.empty();
@@ -62,6 +61,7 @@ public GeospatialIndex(String field) {
 	 * @param name must not be {@literal null} or empty.
 	 * @return this.
 	 */
+	@Contract("_ -> this")
 	public GeospatialIndex named(String name) {
 
 		this.name = name;
@@ -72,6 +72,7 @@ public GeospatialIndex named(String name) {
 	 * @param min
 	 * @return this.
 	 */
+	@Contract("_ -> this")
 	public GeospatialIndex withMin(int min) {
 		this.min = min;
 		return this;
@@ -81,6 +82,7 @@ public GeospatialIndex withMin(int min) {
 	 * @param max
 	 * @return this.
 	 */
+	@Contract("_ -> this")
 	public GeospatialIndex withMax(int max) {
 		this.max = max;
 		return this;
@@ -90,6 +92,7 @@ public GeospatialIndex withMax(int max) {
 	 * @param bits
 	 * @return this.
 	 */
+	@Contract("_ -> this")
 	public GeospatialIndex withBits(int bits) {
 		this.bits = bits;
 		return this;
@@ -99,6 +102,7 @@ public GeospatialIndex withBits(int bits) {
 	 * @param type must not be {@literal null}.
 	 * @return this.
 	 */
+	@Contract("_ -> this")
 	public GeospatialIndex typed(GeoSpatialIndexType type) {
 
 		Assert.notNull(type, "Type must not be null");
@@ -107,21 +111,11 @@ public GeospatialIndex typed(GeoSpatialIndexType type) {
 		return this;
 	}
 
-	/**
-	 * @param bucketSize
-	 * @return this.
-	 * @deprecated since MongoDB server version 4.4
-	 */
-	@Deprecated
-	public GeospatialIndex withBucketSize(double bucketSize) {
-		this.bucketSize = bucketSize;
-		return this;
-	}
-
 	/**
 	 * @param fieldName
 	 * @return this.
 	 */
+	@Contract("_ -> this")
 	public GeospatialIndex withAdditionalField(String fieldName) {
 		this.additionalField = fieldName;
 		return this;
@@ -136,6 +130,7 @@ public GeospatialIndex withAdditionalField(String fieldName) {
 	 *      "https://docs.mongodb.com/manual/core/index-partial/">https://docs.mongodb.com/manual/core/index-partial/</a>
 	 * @since 1.10
 	 */
+	@Contract("_ -> this")
 	public GeospatialIndex partial(@Nullable IndexFilter filter) {
 
 		this.filter = Optional.ofNullable(filter);
@@ -152,6 +147,7 @@ public GeospatialIndex partial(@Nullable IndexFilter filter) {
 	 * @return this.
 	 * @since 2.0
 	 */
+	@Contract("_ -> this")
 	public GeospatialIndex collation(@Nullable Collation collation) {
 
 		this.collation = Optional.ofNullable(collation);
@@ -203,14 +199,9 @@ public Document getIndexOptions() {
 				break;
 
 			case GEO_2DSPHERE:
-
 				break;
 
 			case GEO_HAYSTACK:
-
-				if (bucketSize != null) {
-					document.put("bucketSize", bucketSize);
-				}
 				break;
 		}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java
index 95f4226e28..91195a40f4 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java
@@ -23,10 +23,11 @@
 import java.util.concurrent.TimeUnit;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.domain.Sort.Direction;
 import org.springframework.data.mongodb.core.index.IndexOptions.Unique;
 import org.springframework.data.mongodb.core.query.Collation;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 
@@ -52,11 +53,13 @@ public Index(String key, Direction direction) {
 		fieldSpec.put(key, direction);
 	}
 
+	@Contract("_, _ -> this")
 	public Index on(String key, Direction direction) {
 		fieldSpec.put(key, direction);
 		return this;
 	}
 
+	@Contract("_ -> this")
 	public Index named(String name) {
 		this.name = name;
 		return this;
@@ -69,6 +72,7 @@ public Index named(String name) {
 	 * @see <a href=
 	 *      "https://docs.mongodb.org/manual/core/index-unique/">https://docs.mongodb.org/manual/core/index-unique/</a>
 	 */
+	@Contract("-> this")
 	public Index unique() {
 
 		this.options.setUnique(Unique.YES);
@@ -82,6 +86,7 @@ public Index unique() {
 	 * @see <a href=
 	 *      "https://docs.mongodb.org/manual/core/index-sparse/">https://docs.mongodb.org/manual/core/index-sparse/</a>
 	 */
+	@Contract("-> this")
 	public Index sparse() {
 		this.sparse = true;
 		return this;
@@ -92,7 +97,7 @@ public Index sparse() {
 	 *
 	 * @return this.
 	 * @since 1.5
-	 */
+	 */@Contract("-> this")
 	public Index background() {
 
 		this.background = true;
@@ -107,6 +112,7 @@ public Index background() {
 	 *      "https://www.mongodb.com/docs/manual/core/index-hidden/">https://www.mongodb.com/docs/manual/core/index-hidden/</a>
 	 * @since 4.1
 	 */
+	@Contract("-> this")
 	public Index hidden() {
 
 		options.setHidden(true);
@@ -120,6 +126,7 @@ public Index hidden() {
 	 * @return this.
 	 * @since 1.5
 	 */
+	@Contract("_ -> this")
 	public Index expire(long value) {
 		return expire(value, TimeUnit.SECONDS);
 	}
@@ -132,6 +139,7 @@ public Index expire(long value) {
 	 * @throws IllegalArgumentException if given {@literal timeout} is {@literal null}.
 	 * @since 2.2
 	 */
+	@Contract("_ -> this")
 	public Index expire(Duration timeout) {
 
 		Assert.notNull(timeout, "Timeout must not be null");
@@ -146,6 +154,7 @@ public Index expire(Duration timeout) {
 	 * @return this.
 	 * @since 1.5
 	 */
+	@Contract("_, _ -> this")
 	public Index expire(long value, TimeUnit unit) {
 
 		Assert.notNull(unit, "TimeUnit for expiration must not be null");
@@ -162,6 +171,7 @@ public Index expire(long value, TimeUnit unit) {
 	 *      "https://docs.mongodb.com/manual/core/index-partial/">https://docs.mongodb.com/manual/core/index-partial/</a>
 	 * @since 1.10
 	 */
+	@Contract("_ -> this")
 	public Index partial(@Nullable IndexFilter filter) {
 
 		this.filter = Optional.ofNullable(filter);
@@ -178,6 +188,7 @@ public Index partial(@Nullable IndexFilter filter) {
 	 * @return this.
 	 * @since 2.0
 	 */
+	@Contract("_ -> this")
 	public Index collation(@Nullable Collation collation) {
 
 		this.collation = Optional.ofNullable(collation);
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexField.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexField.java
index a5cbf6c896..2e7268699c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexField.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexField.java
@@ -15,8 +15,8 @@
  */
 package org.springframework.data.mongodb.core.index;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.domain.Sort.Direction;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 
@@ -139,8 +139,7 @@ public String getKey() {
 	 *
 	 * @return the direction
 	 */
-	@Nullable
-	public Direction getDirection() {
+	public @Nullable Direction getDirection() {
 		return direction;
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java
index de7153bfb5..e9817746c3 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java
@@ -27,11 +27,12 @@
 import java.util.stream.Collectors;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.util.BsonUtils;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.NumberUtils;
 import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
 
 /**
  * Index information for a MongoDB index.
@@ -89,7 +90,7 @@ public IndexInfo(List<IndexField> indexFields, String name, boolean unique, bool
 	 */
 	public static IndexInfo indexInfoOf(Document sourceDocument) {
 
-		Document keyDbObject = (Document) sourceDocument.get("key");
+		Document keyDbObject = sourceDocument.get("key", new Document());
 		int numberOfElements = keyDbObject.keySet().size();
 
 		List<IndexField> indexFields = new ArrayList<IndexField>(numberOfElements);
@@ -105,9 +106,10 @@ public static IndexInfo indexInfoOf(Document sourceDocument) {
 			} else if ("text".equals(value)) {
 
 				Document weights = (Document) sourceDocument.get("weights");
-
-				for (String fieldName : weights.keySet()) {
-					indexFields.add(IndexField.text(fieldName, Float.valueOf(weights.get(fieldName).toString())));
+				if(weights != null) {
+					for (String fieldName : weights.keySet()) {
+						indexFields.add(IndexField.text(fieldName, Float.valueOf(weights.get(fieldName).toString())));
+					}
 				}
 
 			} else {
@@ -129,7 +131,7 @@ public static IndexInfo indexInfoOf(Document sourceDocument) {
 			}
 		}
 
-		String name = sourceDocument.get("name").toString();
+		String name = ObjectUtils.nullSafeToString(sourceDocument.get("name"));
 
 		boolean unique = sourceDocument.get("unique", false);
 		boolean sparse = sourceDocument.get("sparse", false);
@@ -161,8 +163,7 @@ public static IndexInfo indexInfoOf(Document sourceDocument) {
 	 * @return the {@link String} representation of the partial filter {@link Document}.
 	 * @since 2.1.11
 	 */
-	@Nullable
-	private static String extractPartialFilterString(Document sourceDocument) {
+	private static @Nullable String extractPartialFilterString(Document sourceDocument) {
 
 		if (!sourceDocument.containsKey("partialFilterExpression")) {
 			return null;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperationsProvider.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperationsProvider.java
index ca3d951c94..aec1ba817d 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperationsProvider.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperationsProvider.java
@@ -15,7 +15,7 @@
  */
 package org.springframework.data.mongodb.core.index;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /**
  * Provider interface to obtain {@link IndexOperations} by MongoDB collection name or entity type.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOptions.java
index 887542cb0c..a390d1eb3e 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOptions.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOptions.java
@@ -18,7 +18,7 @@
 import java.time.Duration;
 
 import org.bson.Document;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /**
  * Changeable properties of an index. Can be used for index creation and modification.
@@ -28,14 +28,11 @@
  */
 public class IndexOptions {
 
-	@Nullable
-	private Duration expire;
+	private @Nullable Duration expire;
 
-	@Nullable
-	private Boolean hidden;
+	private @Nullable Boolean hidden;
 
-	@Nullable
-	private Unique unique;
+	private @Nullable Unique unique;
 
 	public enum Unique {
 
@@ -108,8 +105,7 @@ public void setExpire(Duration expire) {
 	/**
 	 * @return {@literal true} if hidden, {@literal null} if not set.
 	 */
-	@Nullable
-	public Boolean isHidden() {
+	public @Nullable Boolean isHidden() {
 		return hidden;
 	}
 
@@ -123,8 +119,7 @@ public void setHidden(boolean hidden) {
 	/**
 	 * @return the unique property value, {@literal null} if not set.
 	 */
-	@Nullable
-	public Unique getUnique() {
+	public @Nullable Unique getUnique() {
 		return unique;
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexPredicate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexPredicate.java
index 362247725f..3bb3fdbd0f 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexPredicate.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexPredicate.java
@@ -15,7 +15,7 @@
  */
 package org.springframework.data.mongodb.core.index;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /**
  * @author Jon Brisbin <jbrisbin@vmware.com>
@@ -26,8 +26,7 @@ public abstract class IndexPredicate {
 	private IndexDirection direction = IndexDirection.ASCENDING;
 	private boolean unique = false;
 
-	@Nullable
-	public String getName() {
+	public @Nullable String getName() {
 		return name;
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java
index e20b0704cc..f1550d1501 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java
@@ -21,7 +21,7 @@
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
-
+import org.jspecify.annotations.Nullable;
 import org.springframework.context.ApplicationListener;
 import org.springframework.dao.DataIntegrityViolationException;
 import org.springframework.data.mapping.PersistentEntity;
@@ -33,7 +33,6 @@
 import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
 import org.springframework.data.mongodb.util.MongoDbErrorCodes;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 
@@ -183,8 +182,7 @@ public boolean isIndexCreatorFor(MappingContext<?, ?> context) {
 		return this.mappingContext.equals(context);
 	}
 
-	@Nullable
-	private IndexInfo fetchIndexInformation(@Nullable IndexDefinitionHolder indexDefinition) {
+	private @Nullable IndexInfo fetchIndexInformation(@Nullable IndexDefinitionHolder indexDefinition) {
 
 		if (indexDefinition == null) {
 			return null;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java
index a5988b8c1d..b7beaaa3e1 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java
@@ -25,7 +25,6 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Supplier;
@@ -33,6 +32,7 @@
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
+import org.jspecify.annotations.Nullable;
 import org.springframework.core.annotation.MergedAnnotation;
 import org.springframework.dao.InvalidDataAccessApiUsageException;
 import org.springframework.data.domain.Sort;
@@ -55,13 +55,11 @@
 import org.springframework.data.mongodb.util.BsonUtils;
 import org.springframework.data.mongodb.util.DotPath;
 import org.springframework.data.mongodb.util.DurationUtil;
-import org.springframework.data.mongodb.util.MongoClientVersion;
 import org.springframework.data.mongodb.util.spel.ExpressionUtils;
 import org.springframework.data.spel.EvaluationContextProvider;
 import org.springframework.data.util.TypeInformation;
 import org.springframework.expression.EvaluationContext;
 import org.springframework.expression.spel.standard.SpelExpressionParser;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.ObjectUtils;
@@ -112,7 +110,7 @@ public Iterable<? extends IndexDefinitionHolder> resolveIndexFor(TypeInformation
 	 * {@link GeospatialIndex}. The given {@literal root} has therefore to be annotated with {@link Document}.
 	 *
 	 * @param root must not be null.
-	 * @return List of {@link IndexDefinitionHolder}. Will never be {@code null}.
+	 * @return List of {@link IndexDefinitionHolder}. Will never be {@literal null}.
 	 * @throws IllegalArgumentException in case of missing {@link Document} annotation marking root entities.
 	 */
 	public List<IndexDefinitionHolder> resolveIndexForEntity(MongoPersistentEntity<?> root) {
@@ -165,7 +163,7 @@ private void potentiallyAddIndexForProperty(MongoPersistentEntity<?> root, Mongo
 			}
 
 			if (persistentProperty.isEntity()) {
-				indexes.addAll(resolveIndexForEntity(mappingContext.getPersistentEntity(persistentProperty),
+				indexes.addAll(resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(persistentProperty),
 						persistentProperty.isUnwrapped() ? "" : persistentProperty.getFieldName(), Path.of(persistentProperty),
 						root.getCollection(), guard));
 			}
@@ -191,7 +189,7 @@ private void potentiallyAddIndexForProperty(MongoPersistentEntity<?> root, Mongo
 	 * @param collection
 	 * @param guard
 	 * @return List of {@link IndexDefinitionHolder} representing indexes for given type and its referenced property
-	 *         types. Will never be {@code null}.
+	 *         types. Will never be {@literal null}.
 	 */
 	private List<IndexDefinitionHolder> resolveIndexForClass(TypeInformation<?> type, String dotPath, Path path,
 			String collection, CycleGuard guard) {
@@ -232,7 +230,7 @@ private void guardAndPotentiallyAddIndexForProperty(MongoPersistentProperty pers
 
 		if (persistentProperty.isEntity()) {
 			try {
-				indexes.addAll(resolveIndexForEntity(mappingContext.getPersistentEntity(persistentProperty),
+				indexes.addAll(resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(persistentProperty),
 						propertyDotPath.toString(), propertyPath, collection, guard));
 			} catch (CyclicPropertyReferenceException e) {
 				LOGGER.info(e.getMessage());
@@ -385,7 +383,7 @@ public void doWithPersistentProperty(MongoPersistentProperty persistentProperty)
 
 						try {
 							appendTextIndexInformation(propertyDotPath, propertyPath, indexDefinitionBuilder,
-									mappingContext.getPersistentEntity(persistentProperty.getActualType()), optionsForNestedType, guard);
+									mappingContext.getRequiredPersistentEntity(persistentProperty.getActualType()), optionsForNestedType, guard);
 						} catch (CyclicPropertyReferenceException e) {
 							LOGGER.info(e.getMessage());
 						} catch (InvalidDataAccessApiUsageException e) {
@@ -520,8 +518,7 @@ private org.bson.Document resolveCompoundIndexKeyFromStringDefinition(String dot
 	 * @param persistentProperty
 	 * @return
 	 */
-	@Nullable
-	protected IndexDefinitionHolder createIndexDefinition(String dotPath, String collection,
+	protected @Nullable IndexDefinitionHolder createIndexDefinition(String dotPath, String collection,
 			MongoPersistentProperty persistentProperty) {
 
 		Indexed index = persistentProperty.findAnnotation(Indexed.class);
@@ -577,7 +574,7 @@ protected IndexDefinitionHolder createIndexDefinition(String dotPath, String col
 		return new IndexDefinitionHolder(dotPath, indexDefinition, collection);
 	}
 
-	private PartialIndexFilter evaluatePartialFilter(String filterExpression, PersistentEntity<?, ?> entity) {
+	private PartialIndexFilter evaluatePartialFilter(String filterExpression, @Nullable PersistentEntity<?, ?> entity) {
 
 		Object result = ExpressionUtils.evaluate(filterExpression, () -> getEvaluationContextForProperty(entity));
 
@@ -588,7 +585,7 @@ private PartialIndexFilter evaluatePartialFilter(String filterExpression, Persis
 		return PartialIndexFilter.of(BsonUtils.parse(filterExpression, null));
 	}
 
-	private org.bson.Document evaluateWildcardProjection(String projectionExpression, PersistentEntity<?, ?> entity) {
+	private org.bson.Document evaluateWildcardProjection(String projectionExpression, @Nullable PersistentEntity<?, ?> entity) {
 
 		Object result = ExpressionUtils.evaluate(projectionExpression, () -> getEvaluationContextForProperty(entity));
 
@@ -599,7 +596,7 @@ private org.bson.Document evaluateWildcardProjection(String projectionExpression
 		return BsonUtils.parse(projectionExpression, null);
 	}
 
-	private Collation evaluateCollation(String collationExpression, PersistentEntity<?, ?> entity) {
+	private Collation evaluateCollation(String collationExpression, @Nullable PersistentEntity<?, ?> entity) {
 
 		Object result = ExpressionUtils.evaluate(collationExpression, () -> getEvaluationContextForProperty(entity));
 		if (result instanceof org.bson.Document document) {
@@ -692,8 +689,7 @@ public void setEvaluationContextProvider(EvaluationContextProvider evaluationCon
 	 * @param persistentProperty
 	 * @return
 	 */
-	@Nullable
-	protected IndexDefinitionHolder createGeoSpatialIndexDefinition(String dotPath, String collection,
+	protected @Nullable IndexDefinitionHolder createGeoSpatialIndexDefinition(String dotPath, String collection,
 			MongoPersistentProperty persistentProperty) {
 
 		GeoSpatialIndexed index = persistentProperty.findAnnotation(GeoSpatialIndexed.class);
@@ -711,23 +707,6 @@ protected IndexDefinitionHolder createGeoSpatialIndexDefinition(String dotPath,
 					.named(pathAwareIndexName(index.name(), dotPath, persistentProperty.getOwner(), persistentProperty));
 		}
 
-		if (MongoClientVersion.isVersion5orNewer()) {
-
-			Optional<Double> defaultBucketSize = MergedAnnotation.of(GeoSpatialIndexed.class).getDefaultValue("bucketSize",
-					Double.class);
-			if (!defaultBucketSize.isPresent() || index.bucketSize() != defaultBucketSize.get()) {
-				indexDefinition.withBucketSize(index.bucketSize());
-			} else {
-				if (LOGGER.isInfoEnabled()) {
-					LOGGER.info(
-							"GeoSpatialIndexed.bucketSize no longer supported by Mongo Client 5 or newer. Ignoring bucketSize for path %s."
-									.formatted(dotPath));
-				}
-			}
-		} else {
-			indexDefinition.withBucketSize(index.bucketSize());
-		}
-
 		indexDefinition.typed(index.type()).withAdditionalField(index.additionalField());
 
 		return new IndexDefinitionHolder(dotPath, indexDefinition, collection);
@@ -812,8 +791,7 @@ private static Duration computeIndexTimeout(String timeoutValue, Supplier<Evalua
 	 * @return the collation present on either the annotation or the entity as a fallback. Might be {@literal null}.
 	 * @since 4.0
 	 */
-	@Nullable
-	private Collation resolveCollation(Annotation annotation, @Nullable PersistentEntity<?, ?> entity) {
+	private @Nullable Collation resolveCollation(Annotation annotation, @Nullable PersistentEntity<?, ?> entity) {
 		return MergedAnnotation.from(annotation).getValue("collation", String.class).filter(StringUtils::hasText)
 				.map(it -> evaluateCollation(it, entity)).orElseGet(() -> {
 
@@ -1138,8 +1116,7 @@ public IncludeStrategy getStrategy() {
 			return strategy;
 		}
 
-		@Nullable
-		public TextIndexedFieldSpec getParentFieldSpec() {
+		public @Nullable TextIndexedFieldSpec getParentFieldSpec() {
 			return parentFieldSpec;
 		}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/SearchIndexDefinition.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/SearchIndexDefinition.java
index 9d4315beae..e3ea12baa9 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/SearchIndexDefinition.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/SearchIndexDefinition.java
@@ -16,11 +16,11 @@
 package org.springframework.data.mongodb.core.index;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mapping.context.MappingContext;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
 import org.springframework.data.util.TypeInformation;
-import org.springframework.lang.Nullable;
 
 /**
  * Definition for an Atlas Search Index (Search Index or Vector Index).
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/SearchIndexInfo.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/SearchIndexInfo.java
index 1a657ecf0b..6da94dd130 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/SearchIndexInfo.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/SearchIndexInfo.java
@@ -18,12 +18,12 @@
 import java.util.function.Supplier;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mapping.context.MappingContext;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
 import org.springframework.data.util.Lazy;
 import org.springframework.data.util.TypeInformation;
-import org.springframework.lang.Nullable;
 
 /**
  * Index information for a MongoDB Search Index.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/TextIndexDefinition.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/TextIndexDefinition.java
index a87b15de45..0b473388fb 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/TextIndexDefinition.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/TextIndexDefinition.java
@@ -20,9 +20,10 @@
 import java.util.Set;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.dao.InvalidDataAccessApiUsageException;
 import org.springframework.data.mongodb.core.query.Collation;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
@@ -235,6 +236,7 @@ public TextIndexDefinitionBuilder() {
 		 * @param name
 		 * @return
 		 */
+		@Contract("_ -> this")
 		public TextIndexDefinitionBuilder named(String name) {
 			this.instance.name = name;
 			return this;
@@ -246,6 +248,7 @@ public TextIndexDefinitionBuilder named(String name) {
 		 *
 		 * @return
 		 */
+		@Contract("-> this")
 		public TextIndexDefinitionBuilder onAllFields() {
 
 			if (!instance.fieldSpecs.isEmpty()) {
@@ -262,6 +265,7 @@ public TextIndexDefinitionBuilder onAllFields() {
 		 * @param fieldnames
 		 * @return
 		 */
+		@Contract("_ -> this")
 		public TextIndexDefinitionBuilder onFields(String... fieldnames) {
 
 			for (String fieldname : fieldnames) {
@@ -276,6 +280,7 @@ public TextIndexDefinitionBuilder onFields(String... fieldnames) {
 		 * @param fieldname
 		 * @return
 		 */
+		@Contract("_ -> this")
 		public TextIndexDefinitionBuilder onField(String fieldname) {
 			return onField(fieldname, 1F);
 		}
@@ -286,6 +291,7 @@ public TextIndexDefinitionBuilder onField(String fieldname) {
 		 * @param fieldname
 		 * @return
 		 */
+		@Contract("_, _ -> this")
 		public TextIndexDefinitionBuilder onField(String fieldname, Float weight) {
 
 			if (this.instance.fieldSpecs.contains(ALL_FIELDS)) {
@@ -305,6 +311,7 @@ public TextIndexDefinitionBuilder onField(String fieldname, Float weight) {
 		 * @see <a href=
 		 *      "https://docs.mongodb.org/manual/tutorial/specify-language-for-text-index/#specify-default-language-text-index">https://docs.mongodb.org/manual/tutorial/specify-language-for-text-index/#specify-default-language-text-index</a>
 		 */
+		@Contract("_ -> this")
 		public TextIndexDefinitionBuilder withDefaultLanguage(String language) {
 
 			this.instance.defaultLanguage = language;
@@ -317,6 +324,7 @@ public TextIndexDefinitionBuilder withDefaultLanguage(String language) {
 		 * @param fieldname
 		 * @return
 		 */
+		@Contract("_ -> this")
 		public TextIndexDefinitionBuilder withLanguageOverride(String fieldname) {
 
 			if (StringUtils.hasText(this.instance.languageOverride)) {
@@ -338,6 +346,7 @@ public TextIndexDefinitionBuilder withLanguageOverride(String fieldname) {
 		 *      "https://docs.mongodb.com/manual/core/index-partial/">https://docs.mongodb.com/manual/core/index-partial/</a>
 		 * @since 1.10
 		 */
+		@Contract("_ -> this")
 		public TextIndexDefinitionBuilder partial(@Nullable IndexFilter filter) {
 
 			this.instance.filter = filter;
@@ -349,12 +358,14 @@ public TextIndexDefinitionBuilder partial(@Nullable IndexFilter filter) {
 		 *
 		 * @since 2.2
 		 */
+		@Contract("-> this")
 		public TextIndexDefinitionBuilder withSimpleCollation() {
 
 			this.instance.collation = Collation.simple();
 			return this;
 		}
 
+		@Contract("-> new")
 		public TextIndexDefinition build() {
 			return this.instance;
 		}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/VectorIndex.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/VectorIndex.java
index b46dbf4d0c..aa8daa8c39 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/VectorIndex.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/VectorIndex.java
@@ -20,14 +20,15 @@
 import java.util.function.Consumer;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mapping.context.MappingContext;
 import org.springframework.data.mongodb.core.convert.QueryMapper;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
 import org.springframework.data.util.TypeInformation;
 import org.springframework.lang.Contract;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
 
 /**
@@ -149,7 +150,8 @@ static VectorIndex of(Document document) {
 
 		for (Object entry : definition.get("fields", List.class)) {
 			if (entry instanceof Document field) {
-				if (field.get("type").equals("vector")) {
+				Object fieldType = field.get("type");
+				if (ObjectUtils.nullSafeEquals(fieldType, "vector")) {
 					index.addField(new VectorIndexField(field.getString("path"), "vector", field.getInteger("numDimensions"),
 							field.getString("similarity"), field.getString("quantization")));
 				} else {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java
index dcd2b7c022..ff0a92ada1 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java
@@ -21,8 +21,9 @@
 import java.util.concurrent.TimeUnit;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.mapping.FieldName;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.CollectionUtils;
 import org.springframework.util.StringUtils;
 
@@ -76,6 +77,7 @@ public WildcardIndex(@Nullable String path) {
 	 *
 	 * @return this.
 	 */
+	@Contract("-> this")
 	public WildcardIndex includeId() {
 
 		wildcardProjection.put(FieldName.ID.name(), 1);
@@ -89,6 +91,7 @@ public WildcardIndex includeId() {
 	 * @return this.
 	 */
 	@Override
+	@Contract("_ -> this")
 	public WildcardIndex named(String name) {
 
 		super.named(name);
@@ -101,6 +104,7 @@ public WildcardIndex named(String name) {
 	 * @throws UnsupportedOperationException not supported for wildcard indexes.
 	 */
 	@Override
+	@Contract("-> fail")
 	public Index unique() {
 		throw new UnsupportedOperationException("Wildcard Index does not support 'unique'");
 	}
@@ -111,6 +115,7 @@ public Index unique() {
 	 * @throws UnsupportedOperationException not supported for wildcard indexes.
 	 */
 	@Override
+	@Contract("-> fail")
 	public Index expire(long seconds) {
 		throw new UnsupportedOperationException("Wildcard Index does not support 'ttl'");
 	}
@@ -121,6 +126,7 @@ public Index expire(long seconds) {
 	 * @throws UnsupportedOperationException not supported for wildcard indexes.
 	 */
 	@Override
+	@Contract("_, _ -> fail")
 	public Index expire(long value, TimeUnit timeUnit) {
 		throw new UnsupportedOperationException("Wildcard Index does not support 'ttl'");
 	}
@@ -131,6 +137,7 @@ public Index expire(long value, TimeUnit timeUnit) {
 	 * @throws UnsupportedOperationException not supported for wildcard indexes.
 	 */
 	@Override
+	@Contract("_ -> fail")
 	public Index expire(Duration duration) {
 		throw new UnsupportedOperationException("Wildcard Index does not support 'ttl'");
 	}
@@ -142,6 +149,7 @@ public Index expire(Duration duration) {
 	 * @param paths must not be {@literal null}.
 	 * @return this.
 	 */
+	@Contract("_ -> this")
 	public WildcardIndex wildcardProjectionInclude(String... paths) {
 
 		for (String path : paths) {
@@ -157,6 +165,7 @@ public WildcardIndex wildcardProjectionInclude(String... paths) {
 	 * @param paths must not be {@literal null}.
 	 * @return this.
 	 */
+	@Contract("_ -> this")
 	public WildcardIndex wildcardProjectionExclude(String... paths) {
 
 		for (String path : paths) {
@@ -172,6 +181,7 @@ public WildcardIndex wildcardProjectionExclude(String... paths) {
 	 * @param includeExclude must not be {@literal null}.
 	 * @return this.
 	 */
+	@Contract("_ -> this")
 	public WildcardIndex wildcardProjection(Map<String, Object> includeExclude) {
 
 		wildcardProjection.putAll(includeExclude);
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/package-info.java
index c49f501d8d..8524ee62f7 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/package-info.java
@@ -1,6 +1,6 @@
 /**
  * Support for MongoDB document indexing.
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 package org.springframework.data.mongodb.core.index;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java
index 3d68dbaac2..e865009319 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java
@@ -25,6 +25,7 @@
 import java.util.List;
 import java.util.Map;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.annotation.Id;
 import org.springframework.data.expression.ValueEvaluationContext;
 import org.springframework.data.expression.ValueExpression;
@@ -35,6 +36,7 @@
 import org.springframework.data.mapping.PropertyHandler;
 import org.springframework.data.mapping.model.BasicPersistentEntity;
 import org.springframework.data.mongodb.MongoCollectionUtils;
+import org.springframework.data.mongodb.core.query.Collation;
 import org.springframework.data.mongodb.util.encryption.EncryptionUtils;
 import org.springframework.data.spel.ExpressionDependencies;
 import org.springframework.data.util.Lazy;
@@ -42,7 +44,6 @@
 import org.springframework.expression.EvaluationContext;
 import org.springframework.expression.Expression;
 import org.springframework.expression.spel.standard.SpelExpressionParser;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.ObjectUtils;
@@ -139,9 +140,8 @@ public String getLanguage() {
 		return this.language;
 	}
 
-	@Nullable
 	@Override
-	public MongoPersistentProperty getTextScoreProperty() {
+	public @Nullable MongoPersistentProperty getTextScoreProperty() {
 		return getPersistentProperty(TextScore.class);
 	}
 
@@ -151,7 +151,7 @@ public boolean hasTextScoreProperty() {
 	}
 
 	@Override
-	public org.springframework.data.mongodb.core.query.Collation getCollation() {
+	public @Nullable Collation getCollation() {
 
 		Object collationValue = collationExpression != null
 				? collationExpression.evaluate(getValueEvaluationContext(null))
@@ -189,22 +189,22 @@ public void verify() {
 	}
 
 	@Override
-	public EvaluationContext getEvaluationContext(Object rootObject) {
+	public EvaluationContext getEvaluationContext(@Nullable Object rootObject) {
 		return super.getEvaluationContext(rootObject);
 	}
 
 	@Override
-	public EvaluationContext getEvaluationContext(Object rootObject, ExpressionDependencies dependencies) {
+	public EvaluationContext getEvaluationContext(@Nullable Object rootObject, ExpressionDependencies dependencies) {
 		return super.getEvaluationContext(rootObject, dependencies);
 	}
 
 	@Override
-	public ValueEvaluationContext getValueEvaluationContext(Object rootObject) {
+	public ValueEvaluationContext getValueEvaluationContext(@Nullable Object rootObject) {
 		return super.getValueEvaluationContext(rootObject);
 	}
 
 	@Override
-	public ValueEvaluationContext getValueEvaluationContext(Object rootObject, ExpressionDependencies dependencies) {
+	public ValueEvaluationContext getValueEvaluationContext(@Nullable Object rootObject, ExpressionDependencies dependencies) {
 		return super.getValueEvaluationContext(rootObject, dependencies);
 	}
 
@@ -243,7 +243,11 @@ public int compare(@Nullable MongoPersistentProperty o1, @Nullable MongoPersiste
 				return -1;
 			}
 
-			return o1.getFieldOrder() - o2.getFieldOrder();
+			if(o1 != null && o2 != null) {
+				return o1.getFieldOrder() - o2.getFieldOrder();
+			}
+
+			return o1 != null ? o1.getFieldOrder() : -1;
 		}
 	}
 
@@ -257,7 +261,7 @@ public int compare(@Nullable MongoPersistentProperty o1, @Nullable MongoPersiste
 	 * @return can be {@literal null}.
 	 */
 	@Override
-	protected MongoPersistentProperty returnPropertyIfBetterIdPropertyCandidateOrNull(MongoPersistentProperty property) {
+	protected @Nullable MongoPersistentProperty returnPropertyIfBetterIdPropertyCandidateOrNull(MongoPersistentProperty property) {
 
 		Assert.notNull(property, "MongoPersistentProperty must not be null");
 
@@ -268,7 +272,7 @@ protected MongoPersistentProperty returnPropertyIfBetterIdPropertyCandidateOrNul
 		MongoPersistentProperty currentIdProperty = getIdProperty();
 
 		boolean currentIdPropertyIsSet = currentIdProperty != null;
-		@SuppressWarnings("null")
+		@SuppressWarnings("NullAway")
 		boolean currentIdPropertyIsExplicit = currentIdPropertyIsSet && currentIdProperty.isExplicitIdProperty();
 		boolean newIdPropertyIsExplicit = property.isExplicitIdProperty();
 
@@ -277,7 +281,7 @@ protected MongoPersistentProperty returnPropertyIfBetterIdPropertyCandidateOrNul
 
 		}
 
-		@SuppressWarnings("null")
+		@SuppressWarnings("NullAway")
 		Field currentIdPropertyField = currentIdProperty.getField();
 
 		if (newIdPropertyIsExplicit && currentIdPropertyIsExplicit) {
@@ -308,8 +312,7 @@ protected MongoPersistentProperty returnPropertyIfBetterIdPropertyCandidateOrNul
 	 * @param potentialExpression can be {@literal null}
 	 * @return can be {@literal null}.
 	 */
-	@Nullable
-	private static ValueExpression detectExpression(@Nullable String potentialExpression) {
+	private static @Nullable ValueExpression detectExpression(@Nullable String potentialExpression) {
 
 		if (!StringUtils.hasText(potentialExpression)) {
 			return null;
@@ -352,7 +355,7 @@ private void assertUniqueness(MongoPersistentProperty property) {
 	}
 
 	@Override
-	public Collection<Object> getEncryptionKeyIds() {
+	public @Nullable Collection<Object> getEncryptionKeyIds() {
 
 		Encrypted encrypted = findAnnotation(Encrypted.class);
 		if (encrypted == null) {
@@ -405,6 +408,7 @@ private static void potentiallyAssertTextScoreType(MongoPersistentProperty persi
 			}
 		}
 
+		@SuppressWarnings("NullAway")
 		private static void potentiallyAssertDBRefTargetType(MongoPersistentProperty persistentProperty) {
 
 			if (persistentProperty.isDbReference() && persistentProperty.getDBRef().lazy()) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java
index 5c3b4e6532..027a570fa3 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java
@@ -23,7 +23,7 @@
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
-
+import org.jspecify.annotations.Nullable;
 import org.springframework.core.env.StandardEnvironment;
 import org.springframework.data.expression.ValueEvaluationContext;
 import org.springframework.data.mapping.Association;
@@ -39,7 +39,6 @@
 import org.springframework.data.util.Lazy;
 import org.springframework.expression.EvaluationContext;
 import org.springframework.expression.spel.support.StandardEvaluationContext;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
 
@@ -153,8 +152,7 @@ public boolean hasExplicitFieldName() {
 		return StringUtils.hasText(getAnnotatedFieldName());
 	}
 
-	@Nullable
-	private String getAnnotatedFieldName() {
+	private @Nullable String getAnnotatedFieldName() {
 
 		org.springframework.data.mongodb.core.mapping.Field annotation = findAnnotation(
 				org.springframework.data.mongodb.core.mapping.Field.class);
@@ -197,9 +195,8 @@ public DBRef getDBRef() {
 		return findAnnotation(DBRef.class);
 	}
 
-	@Nullable
 	@Override
-	public DocumentReference getDocumentReference() {
+	public @Nullable DocumentReference getDocumentReference() {
 		return findAnnotation(DocumentReference.class);
 	}
 
@@ -258,6 +255,7 @@ public MongoField getMongoField() {
 	}
 
 	@Override
+	@SuppressWarnings("NullAway")
 	public Collection<Object> getEncryptionKeyIds() {
 
 		Encrypted encrypted = findAnnotation(Encrypted.class);
@@ -282,6 +280,7 @@ public Collection<Object> getEncryptionKeyIds() {
 		return target;
 	}
 
+	@SuppressWarnings("NullAway")
 	protected MongoField doGetMongoField() {
 
 		MongoFieldBuilder builder = MongoField.builder();
@@ -295,6 +294,7 @@ protected MongoField doGetMongoField() {
 		return builder.build();
 	}
 
+	@SuppressWarnings("NullAway")
 	private String doGetFieldName() {
 
 		if (isIdProperty()) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/CachingMongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/CachingMongoPersistentProperty.java
index 105c38b288..eb8d08db64 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/CachingMongoPersistentProperty.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/CachingMongoPersistentProperty.java
@@ -15,11 +15,11 @@
  */
 package org.springframework.data.mongodb.core.mapping;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mapping.model.FieldNamingStrategy;
 import org.springframework.data.mapping.model.Property;
 import org.springframework.data.mapping.model.SimpleTypeHolder;
 import org.springframework.data.util.Lazy;
-import org.springframework.lang.Nullable;
 
 /**
  * {@link MongoPersistentProperty} caching access to {@link #isIdProperty()} and {@link #getFieldName()}.
@@ -121,12 +121,12 @@ public boolean isDbReference() {
 	}
 
 	@Override
-	public DBRef getDBRef() {
+	public @Nullable DBRef getDBRef() {
 		return dbref.getNullable();
 	}
 
 	@Override
-	public DocumentReference getDocumentReference() {
+	public @Nullable DocumentReference getDocumentReference() {
 		return documentReference.getNullable();
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitEncrypted.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitEncrypted.java
index 5f08e5c787..37d1019f62 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitEncrypted.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitEncrypted.java
@@ -47,6 +47,7 @@
  * </pre>
  *
  * @author Christoph Strobl
+ * @author Ross Lawley
  * @since 4.1
  * @see ValueConverter
  */
@@ -60,7 +61,8 @@
 	 * Define the algorithm to use.
 	 * <p>
 	 * A {@literal Deterministic} algorithm ensures that a given input value always encrypts to the same output while a
-	 * {@literal randomized} one will produce different results every time.
+	 * {@literal randomized} one will produce different results every time.  A {@literal range} algorithm allows for
+	 * the value to be queried whilst encrypted.
 	 * <p>
 	 * Please make sure to use an algorithm that is in line with MongoDB's encryption rules for simple types, complex
 	 * objects and arrays as well as the query limitations that come with each of them.
@@ -91,4 +93,5 @@
 	 */
 	@AliasFor(annotation = ValueConverter.class, value = "value")
 	Class<? extends PropertyValueConverter> value() default MongoEncryptionConverter.class;
+
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoField.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoField.java
index 6f0e1ae4c3..881d741ee4 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoField.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoField.java
@@ -15,7 +15,9 @@
  */
 package org.springframework.data.mongodb.core.mapping;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.mapping.FieldName.Type;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 
@@ -139,7 +141,7 @@ public String toString() {
 	 */
 	public static class MongoFieldBuilder {
 
-		private String name;
+		private @Nullable String name;
 		private Type nameType = Type.PATH;
 		private FieldType type = FieldType.IMPLICIT;
 		private int order = Integer.MAX_VALUE;
@@ -150,6 +152,7 @@ public static class MongoFieldBuilder {
 		 * @param fieldType
 		 * @return
 		 */
+		@Contract("_ -> this")
 		public MongoFieldBuilder fieldType(FieldType fieldType) {
 
 			this.type = fieldType;
@@ -163,6 +166,7 @@ public MongoFieldBuilder fieldType(FieldType fieldType) {
 		 * @param fieldName
 		 * @return
 		 */
+		@Contract("_ -> this")
 		public MongoFieldBuilder name(String fieldName) {
 
 			Assert.hasText(fieldName, "Field name must not be empty");
@@ -178,6 +182,7 @@ public MongoFieldBuilder name(String fieldName) {
 		 * @param path
 		 * @return
 		 */
+		@Contract("_ -> this")
 		public MongoFieldBuilder path(String path) {
 
 			Assert.hasText(path, "Field path (name) must not be empty");
@@ -193,6 +198,7 @@ public MongoFieldBuilder path(String path) {
 		 * @param order
 		 * @return
 		 */
+		@Contract("_ -> this")
 		public MongoFieldBuilder order(int order) {
 
 			this.order = order;
@@ -204,7 +210,10 @@ public MongoFieldBuilder order(int order) {
 		 *
 		 * @return a new {@link MongoField}.
 		 */
+		@Contract("-> new")
 		public MongoField build() {
+
+			Assert.notNull(name, "Name of Field must not be null");
 			return new MongoField(new FieldName(name, nameType), type, order);
 		}
 	}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoMappingContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoMappingContext.java
index 76c0269861..4540493124 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoMappingContext.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoMappingContext.java
@@ -17,6 +17,7 @@
 
 import java.util.AbstractMap;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.beans.BeansException;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.ApplicationContextAware;
@@ -28,7 +29,6 @@
 import org.springframework.data.mapping.model.SimpleTypeHolder;
 import org.springframework.data.util.NullableWrapperConverters;
 import org.springframework.data.util.TypeInformation;
-import org.springframework.lang.Nullable;
 
 /**
  * Default implementation of a {@link MappingContext} for MongoDB using {@link BasicMongoPersistentEntity} and
@@ -46,8 +46,7 @@ public class MongoMappingContext extends AbstractMappingContext<MongoPersistentE
 	private FieldNamingStrategy fieldNamingStrategy = DEFAULT_NAMING_STRATEGY;
 	private boolean autoIndexCreation = false;
 
-	@Nullable
-	private ApplicationContext applicationContext;
+	private @Nullable ApplicationContext applicationContext;
 
 	/**
 	 * Creates a new {@link MongoMappingContext}.
@@ -125,9 +124,8 @@ public void setAutoIndexCreation(boolean autoCreateIndexes) {
 		this.autoIndexCreation = autoCreateIndexes;
 	}
 
-	@Nullable
 	@Override
-	public MongoPersistentEntity<?> getPersistentEntity(MongoPersistentProperty persistentProperty) {
+	public @Nullable MongoPersistentEntity<?> getPersistentEntity(MongoPersistentProperty persistentProperty) {
 
 		MongoPersistentEntity<?> entity = super.getPersistentEntity(persistentProperty);
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java
index e02bd00c8d..f1d67e4ae8 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java
@@ -17,9 +17,10 @@
 
 import java.util.Collection;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mapping.PersistentEntity;
 import org.springframework.data.mapping.model.MutablePersistentEntity;
-import org.springframework.lang.Nullable;
+import org.springframework.data.mongodb.core.query.Collation;
 
 /**
  * MongoDB specific {@link PersistentEntity} abstraction.
@@ -68,8 +69,7 @@ public interface MongoPersistentEntity<T> extends MutablePersistentEntity<T, Mon
 	 * @return {@literal null} if not set.
 	 * @since 2.2
 	 */
-	@Nullable
-	org.springframework.data.mongodb.core.query.Collation getCollation();
+	@Nullable Collation getCollation();
 
 	/**
 	 * @return {@literal true} if the entity is annotated with
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java
index e75ac015aa..cdbf940720 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java
@@ -17,12 +17,12 @@
 
 import java.util.Collection;
 
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.data.annotation.Id;
 import org.springframework.data.mapping.PersistentEntity;
 import org.springframework.data.mapping.PersistentProperty;
-import org.springframework.lang.NonNull;
-import org.springframework.lang.Nullable;
 
 /**
  * MongoDB specific {@link org.springframework.data.mapping.PersistentProperty} extension.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java
index 3b3a520bc3..6b4d9b9e9b 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java
@@ -29,6 +29,7 @@
 import org.bson.types.Decimal128;
 import org.bson.types.ObjectId;
 import org.bson.types.Symbol;
+
 import org.springframework.data.mapping.model.SimpleTypeHolder;
 
 import com.mongodb.DBRef;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/PersistentPropertyTranslator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/PersistentPropertyTranslator.java
index d78494d23b..01bb7792fa 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/PersistentPropertyTranslator.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/PersistentPropertyTranslator.java
@@ -17,8 +17,8 @@
 
 import java.util.function.Predicate;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.util.Predicates;
-import org.springframework.lang.Nullable;
 
 /**
  * Utility to translate a {@link MongoPersistentProperty} into a corresponding property from a different
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Queryable.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Queryable.java
new file mode 100644
index 0000000000..a0c67f7187
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Queryable.java
@@ -0,0 +1,48 @@
+/*
+ * 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.mongodb.core.mapping;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author Christoph Strobl
+ * @since 4.5
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE })
+public @interface Queryable {
+
+	/**
+	 * @return empty {@link String} if not set.
+	 */
+	String queryType() default "";
+
+	/**
+	 * @return empty {@link String} if not set.
+	 */
+	String queryAttributes() default "";
+
+	/**
+	 * Set the contention factor
+	 *
+	 * @return the contention factor
+	 */
+	long contentionFactor() default -1;
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/RangeEncrypted.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/RangeEncrypted.java
new file mode 100644
index 0000000000..8b2eccb6ca
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/RangeEncrypted.java
@@ -0,0 +1,57 @@
+/*
+ * 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.mongodb.core.mapping;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.core.annotation.AliasFor;
+
+/**
+ * @author Christoph Strobl
+ * @author Ross Lawley
+ * @since 4.5
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+@Encrypted(algorithm = "Range")
+@Queryable(queryType = "range")
+public @interface RangeEncrypted {
+
+	/**
+	 * Set the contention factor.
+	 *
+	 * @return the contention factor
+	 */
+	@AliasFor(annotation = Queryable.class, value = "contentionFactor")
+	long contentionFactor() default -1;
+
+	/**
+	 * Set the {@literal range} options.
+	 * <p>
+	 * Should be valid extended {@link org.bson.Document#parse(String) JSON} representing the range options and including
+	 * the following values: {@code min}, {@code max}, {@code trimFactor} and {@code sparsity}.
+	 * <p>
+	 * Please note that values are data type sensitive and may require proper identification via eg. {@code $numberLong}.
+	 *
+	 * @return the {@link org.bson.Document#parse(String) JSON} representation of range options.
+	 */
+	@AliasFor(annotation = Queryable.class, value = "queryAttributes")
+	String rangeOptions() default "";
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ShardKey.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ShardKey.java
index 28a114a918..1976cc35f3 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ShardKey.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ShardKey.java
@@ -21,7 +21,7 @@
 import java.util.List;
 
 import org.bson.Document;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.ObjectUtils;
 
 /**
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrapEntityContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrapEntityContext.java
index b3b73397ff..9b42a0d37e 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrapEntityContext.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrapEntityContext.java
@@ -15,7 +15,7 @@
  */
 package org.springframework.data.mongodb.core.mapping;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.ObjectUtils;
 
 /**
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentEntity.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentEntity.java
index fed08815b8..5c404a9d12 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentEntity.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentEntity.java
@@ -24,6 +24,7 @@
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.core.env.Environment;
 import org.springframework.data.mapping.*;
 import org.springframework.data.mapping.model.PersistentPropertyAccessorFactory;
@@ -31,7 +32,7 @@
 import org.springframework.data.spel.EvaluationContextProvider;
 import org.springframework.data.util.Streamable;
 import org.springframework.data.util.TypeInformation;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 
 /**
  * Unwrapped variant of {@link MongoPersistentEntity}.
@@ -62,8 +63,7 @@ public String getLanguage() {
 	}
 
 	@Override
-	@Nullable
-	public MongoPersistentProperty getTextScoreProperty() {
+	public @Nullable MongoPersistentProperty getTextScoreProperty() {
 		return delegate.getTextScoreProperty();
 	}
 
@@ -73,8 +73,7 @@ public boolean hasTextScoreProperty() {
 	}
 
 	@Override
-	@Nullable
-	public Collation getCollation() {
+	public @Nullable Collation getCollation() {
 		return delegate.getCollation();
 	}
 
@@ -98,13 +97,7 @@ public String getName() {
 		return delegate.getName();
 	}
 
-	@Override
 	@Nullable
-	@Deprecated
-	public PreferredConstructor<T, MongoPersistentProperty> getPersistenceConstructor() {
-		return delegate.getPersistenceConstructor();
-	}
-
 	@Override
 	public InstanceCreatorMetadata<MongoPersistentProperty> getInstanceCreatorMetadata() {
 		return delegate.getInstanceCreatorMetadata();
@@ -126,8 +119,7 @@ public boolean isVersionProperty(PersistentProperty<?> property) {
 	}
 
 	@Override
-	@Nullable
-	public MongoPersistentProperty getIdProperty() {
+	public @Nullable MongoPersistentProperty getIdProperty() {
 		return delegate.getIdProperty();
 	}
 
@@ -137,8 +129,7 @@ public MongoPersistentProperty getRequiredIdProperty() {
 	}
 
 	@Override
-	@Nullable
-	public MongoPersistentProperty getVersionProperty() {
+	public @Nullable MongoPersistentProperty getVersionProperty() {
 		return delegate.getVersionProperty();
 	}
 
@@ -148,8 +139,7 @@ public MongoPersistentProperty getRequiredVersionProperty() {
 	}
 
 	@Override
-	@Nullable
-	public MongoPersistentProperty getPersistentProperty(String name) {
+	public @Nullable MongoPersistentProperty getPersistentProperty(String name) {
 		return wrap(delegate.getPersistentProperty(name));
 	}
 
@@ -165,8 +155,7 @@ public MongoPersistentProperty getRequiredPersistentProperty(String name) {
 	}
 
 	@Override
-	@Nullable
-	public MongoPersistentProperty getPersistentProperty(Class<? extends Annotation> annotationType) {
+	public @Nullable MongoPersistentProperty getPersistentProperty(Class<? extends Annotation> annotationType) {
 		return wrap(delegate.getPersistentProperty(annotationType));
 	}
 
@@ -232,8 +221,7 @@ public void doWithAssociations(SimpleAssociationHandler handler) {
 	}
 
 	@Override
-	@Nullable
-	public <A extends Annotation> A findAnnotation(Class<A> annotationType) {
+	public <A extends Annotation> @Nullable A findAnnotation(Class<A> annotationType) {
 		return delegate.findAnnotation(annotationType);
 	}
 
@@ -295,7 +283,9 @@ public Spliterator<MongoPersistentProperty> spliterator() {
 		return delegate.spliterator();
 	}
 
-	private MongoPersistentProperty wrap(MongoPersistentProperty source) {
+	@Contract("null -> null; !null -> !null")
+	private @Nullable MongoPersistentProperty wrap(@Nullable MongoPersistentProperty source) {
+
 		if (source == null) {
 			return source;
 		}
@@ -338,7 +328,7 @@ public boolean isUnwrapped() {
 	}
 
 	@Override
-	public Collection<Object> getEncryptionKeyIds() {
+	public @Nullable Collection<Object> getEncryptionKeyIds() {
 		return delegate.getEncryptionKeyIds();
 	}
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentProperty.java
index 1d4877478f..ac7f24a555 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentProperty.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentProperty.java
@@ -20,11 +20,11 @@
 import java.lang.reflect.Method;
 import java.util.Collection;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mapping.Association;
 import org.springframework.data.mapping.PersistentEntity;
 import org.springframework.data.mapping.PersistentPropertyAccessor;
 import org.springframework.data.util.TypeInformation;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ObjectUtils;
 
 /**
@@ -47,6 +47,7 @@ public UnwrappedMongoPersistentProperty(MongoPersistentProperty delegate, Unwrap
 	}
 
 	@Override
+	@SuppressWarnings("NullAway")
 	public String getFieldName() {
 
 		if (!context.getProperty().isUnwrapped()) {
@@ -57,6 +58,7 @@ public String getFieldName() {
 	}
 
 	@Override
+	@SuppressWarnings("NullAway")
 	public boolean hasExplicitFieldName() {
 		return delegate.hasExplicitFieldName()
 				|| !ObjectUtils.isEmpty(context.getProperty().findAnnotation(Unwrapped.class).prefix());
@@ -108,14 +110,12 @@ public boolean isTextScoreProperty() {
 	}
 
 	@Override
-	@Nullable
-	public DBRef getDBRef() {
+	public @Nullable DBRef getDBRef() {
 		return delegate.getDBRef();
 	}
 
 	@Override
-	@Nullable
-	public DocumentReference getDocumentReference() {
+	public @Nullable DocumentReference getDocumentReference() {
 		return delegate.getDocumentReference();
 	}
 
@@ -145,6 +145,7 @@ public Class<?> getType() {
 	}
 
 	@Override
+	@SuppressWarnings("NullAway")
 	public MongoField getMongoField() {
 
 		if (!context.getProperty().isUnwrapped()) {
@@ -165,8 +166,7 @@ public Iterable<? extends TypeInformation<?>> getPersistentEntityTypeInformation
 	}
 
 	@Override
-	@Nullable
-	public Method getGetter() {
+	public @Nullable Method getGetter() {
 		return delegate.getGetter();
 	}
 
@@ -176,8 +176,7 @@ public Method getRequiredGetter() {
 	}
 
 	@Override
-	@Nullable
-	public Method getSetter() {
+	public @Nullable Method getSetter() {
 		return delegate.getSetter();
 	}
 
@@ -187,8 +186,7 @@ public Method getRequiredSetter() {
 	}
 
 	@Override
-	@Nullable
-	public Method getWither() {
+	public @Nullable Method getWither() {
 		return delegate.getWither();
 	}
 
@@ -198,8 +196,7 @@ public Method getRequiredWither() {
 	}
 
 	@Override
-	@Nullable
-	public Field getField() {
+	public @Nullable Field getField() {
 		return delegate.getField();
 	}
 
@@ -209,14 +206,12 @@ public Field getRequiredField() {
 	}
 
 	@Override
-	@Nullable
-	public String getSpelExpression() {
+	public @Nullable String getSpelExpression() {
 		return delegate.getSpelExpression();
 	}
 
 	@Override
-	@Nullable
-	public Association<MongoPersistentProperty> getAssociation() {
+	public @Nullable Association<MongoPersistentProperty> getAssociation() {
 		return delegate.getAssociation();
 	}
 
@@ -291,8 +286,7 @@ public Collection<Object> getEncryptionKeyIds() {
 	}
 
 	@Override
-	@Nullable
-	public Class<?> getComponentType() {
+	public @Nullable Class<?> getComponentType() {
 		return delegate.getComponentType();
 	}
 
@@ -302,8 +296,7 @@ public Class<?> getRawType() {
 	}
 
 	@Override
-	@Nullable
-	public Class<?> getMapValueType() {
+	public @Nullable Class<?> getMapValueType() {
 		return delegate.getMapValueType();
 	}
 
@@ -313,8 +306,7 @@ public Class<?> getActualType() {
 	}
 
 	@Override
-	@Nullable
-	public <A extends Annotation> A findAnnotation(Class<A> annotationType) {
+	public <A extends Annotation> @Nullable A findAnnotation(Class<A> annotationType) {
 		return delegate.findAnnotation(annotationType);
 	}
 
@@ -324,8 +316,7 @@ public <A extends Annotation> A getRequiredAnnotation(Class<A> annotationType) t
 	}
 
 	@Override
-	@Nullable
-	public <A extends Annotation> A findPropertyOrOwnerAnnotation(Class<A> annotationType) {
+	public <A extends Annotation> @Nullable A findPropertyOrOwnerAnnotation(Class<A> annotationType) {
 		return delegate.findPropertyOrOwnerAnnotation(annotationType);
 	}
 
@@ -340,13 +331,12 @@ public boolean hasActualTypeAnnotation(Class<? extends Annotation> annotationTyp
 	}
 
 	@Override
-	@Nullable
-	public Class<?> getAssociationTargetType() {
+	public @Nullable Class<?> getAssociationTargetType() {
 		return delegate.getAssociationTargetType();
 	}
 
 	@Override
-	public TypeInformation<?> getAssociationTargetTypeInformation() {
+	public @Nullable TypeInformation<?> getAssociationTargetTypeInformation() {
 		return delegate.getAssociationTargetTypeInformation();
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AbstractDeleteEvent.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AbstractDeleteEvent.java
index 73f4890dec..a8e2c93773 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AbstractDeleteEvent.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AbstractDeleteEvent.java
@@ -16,7 +16,7 @@
 package org.springframework.data.mongodb.core.mapping.event;
 
 import org.bson.Document;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /**
  * Base class for delete events.
@@ -49,8 +49,7 @@ public AbstractDeleteEvent(Document document, @Nullable Class<T> type, String co
 	 *
 	 * @return can be {@literal null}.
 	 */
-	@Nullable
-	public Class<T> getType() {
+	public @Nullable Class<T> getType() {
 		return type;
 	}
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AfterDeleteEvent.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AfterDeleteEvent.java
index 55ccaa5f3f..10f4cdbbb7 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AfterDeleteEvent.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AfterDeleteEvent.java
@@ -16,7 +16,7 @@
 package org.springframework.data.mongodb.core.mapping.event;
 
 import org.bson.Document;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /**
  * Event being thrown after a single or a set of documents has/have been deleted. The {@link Document} held in the event
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeDeleteEvent.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeDeleteEvent.java
index 49d509fb43..c826cadb4e 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeDeleteEvent.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeDeleteEvent.java
@@ -16,7 +16,7 @@
 package org.springframework.data.mongodb.core.mapping.event;
 
 import org.bson.Document;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /**
  * Event being thrown before a document is deleted. The {@link Document} held in the event will represent the query
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/MongoMappingEvent.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/MongoMappingEvent.java
index eec9a3edf1..bec1986720 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/MongoMappingEvent.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/MongoMappingEvent.java
@@ -18,8 +18,8 @@
 import java.util.function.Function;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.context.ApplicationEvent;
-import org.springframework.lang.Nullable;
 
 /**
  * Base {@link ApplicationEvent} triggered by Spring Data MongoDB.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/package-info.java
index 0cc9d071a3..71ed503b20 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/package-info.java
@@ -1,6 +1,6 @@
 /**
  * Mapping event callback infrastructure for the MongoDB document-to-object mapping subsystem.
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 package org.springframework.data.mongodb.core.mapping.event;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/package-info.java
index 0a513f1a18..f5c917d7d7 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/package-info.java
@@ -1,6 +1,6 @@
 /**
  * Infrastructure for the MongoDB document-to-object mapping subsystem.
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 package org.springframework.data.mongodb.core.mapping;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceCounts.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceCounts.java
index 32a9ed5118..ed9c148a1c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceCounts.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceCounts.java
@@ -15,7 +15,7 @@
  */
 package org.springframework.data.mongodb.core.mapreduce;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /**
  * Value object to encapsulate results of a map-reduce count.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceOptions.java
index 9f34ec44e4..2b8c9d1eb3 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceOptions.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceOptions.java
@@ -20,10 +20,11 @@
 import java.util.Optional;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.query.Collation;
-import org.springframework.lang.Nullable;
 
 import com.mongodb.client.model.MapReduceAction;
+import org.springframework.lang.Contract;
 
 /**
  * @author Mark Pollack
@@ -45,7 +46,6 @@ public class MapReduceOptions {
 	private Boolean verbose = Boolean.TRUE;
 	private @Nullable Integer limit;
 
-	private Optional<Boolean> outputSharded = Optional.empty();
 	private Optional<String> finalizeFunction = Optional.empty();
 	private Optional<Collation> collation = Optional.empty();
 
@@ -65,6 +65,7 @@ public static MapReduceOptions options() {
 	 * @param limit Limit the number of objects to process
 	 * @return MapReduceOptions so that methods can be chained in a fluent API style
 	 */
+	@Contract("_ -> this")
 	public MapReduceOptions limit(int limit) {
 
 		this.limit = limit;
@@ -78,6 +79,7 @@ public MapReduceOptions limit(int limit) {
 	 * @param collectionName The name of the collection where the results of the map-reduce operation will be stored.
 	 * @return MapReduceOptions so that methods can be chained in a fluent API style
 	 */
+	@Contract("_ -> this")
 	public MapReduceOptions outputCollection(String collectionName) {
 
 		this.outputCollection = collectionName;
@@ -91,6 +93,7 @@ public MapReduceOptions outputCollection(String collectionName) {
 	 * @param outputDatabase The name of the database where the results of the map-reduce operation will be stored.
 	 * @return MapReduceOptions so that methods can be chained in a fluent API style
 	 */
+	@Contract("_ -> this")
 	public MapReduceOptions outputDatabase(@Nullable String outputDatabase) {
 
 		this.outputDatabase = Optional.ofNullable(outputDatabase);
@@ -105,6 +108,7 @@ public MapReduceOptions outputDatabase(@Nullable String outputDatabase) {
 	 * @return this.
 	 * @since 3.0
 	 */
+	@Contract("-> this")
 	public MapReduceOptions actionInline() {
 
 		this.mapReduceAction = null;
@@ -119,6 +123,7 @@ public MapReduceOptions actionInline() {
 	 * @return this.
 	 * @since 3.0
 	 */
+	@Contract("-> this")
 	public MapReduceOptions actionMerge() {
 
 		this.mapReduceAction = MapReduceAction.MERGE;
@@ -133,6 +138,7 @@ public MapReduceOptions actionMerge() {
 	 * @return this.
 	 * @since 3.0
 	 */
+	@Contract("-> this")
 	public MapReduceOptions actionReduce() {
 
 		this.mapReduceAction = MapReduceAction.REDUCE;
@@ -146,31 +152,20 @@ public MapReduceOptions actionReduce() {
 	 * @return MapReduceOptions so that methods can be chained in a fluent API style
 	 * @since 3.0
 	 */
+	@Contract("-> this")
 	public MapReduceOptions actionReplace() {
 
 		this.mapReduceAction = MapReduceAction.REPLACE;
 		return this;
 	}
 
-	/**
-	 * If true and combined with an output mode that writes to a collection, the output collection will be sharded using
-	 * the _id field. For MongoDB 1.9+
-	 *
-	 * @param outputShared if true, output will be sharded based on _id key.
-	 * @return MapReduceOptions so that methods can be chained in a fluent API style
-	 */
-	public MapReduceOptions outputSharded(boolean outputShared) {
-
-		this.outputSharded = Optional.of(outputShared);
-		return this;
-	}
-
 	/**
 	 * Sets the finalize function
 	 *
 	 * @param finalizeFunction The finalize function. Can be a JSON string or a Spring Resource URL
 	 * @return MapReduceOptions so that methods can be chained in a fluent API style
 	 */
+	@Contract("_ -> this")
 	public MapReduceOptions finalizeFunction(@Nullable String finalizeFunction) {
 
 		this.finalizeFunction = Optional.ofNullable(finalizeFunction);
@@ -184,6 +179,7 @@ public MapReduceOptions finalizeFunction(@Nullable String finalizeFunction) {
 	 * @param scopeVariables variables that can be accessed from map, reduce, and finalize scripts
 	 * @return MapReduceOptions so that methods can be chained in a fluent API style
 	 */
+	@Contract("_ -> this")
 	public MapReduceOptions scopeVariables(Map<String, Object> scopeVariables) {
 
 		this.scopeVariables = scopeVariables;
@@ -197,6 +193,7 @@ public MapReduceOptions scopeVariables(Map<String, Object> scopeVariables) {
 	 * @param javaScriptMode if true, have the execution of map-reduce stay in JavaScript
 	 * @return MapReduceOptions so that methods can be chained in a fluent API style
 	 */
+	@Contract("_ -> this")
 	public MapReduceOptions javaScriptMode(boolean javaScriptMode) {
 
 		this.jsMode = javaScriptMode;
@@ -208,6 +205,7 @@ public MapReduceOptions javaScriptMode(boolean javaScriptMode) {
 	 *
 	 * @return MapReduceOptions so that methods can be chained in a fluent API style
 	 */
+	@Contract("_ -> this")
 	public MapReduceOptions verbose(boolean verbose) {
 
 		this.verbose = verbose;
@@ -221,6 +219,7 @@ public MapReduceOptions verbose(boolean verbose) {
 	 * @return
 	 * @since 2.0
 	 */
+	@Contract("_ -> this")
 	public MapReduceOptions collation(@Nullable Collation collation) {
 
 		this.collation = Optional.ofNullable(collation);
@@ -231,13 +230,11 @@ public Optional<String> getFinalizeFunction() {
 		return this.finalizeFunction;
 	}
 
-	@Nullable
-	public Boolean getJavaScriptMode() {
+	public @Nullable Boolean getJavaScriptMode() {
 		return this.jsMode;
 	}
 
-	@Nullable
-	public String getOutputCollection() {
+	public @Nullable String getOutputCollection() {
 		return this.outputCollection;
 	}
 
@@ -245,10 +242,6 @@ public Optional<String> getOutputDatabase() {
 		return this.outputDatabase;
 	}
 
-	public Optional<Boolean> getOutputSharded() {
-		return this.outputSharded;
-	}
-
 	public Map<String, Object> getScopeVariables() {
 		return this.scopeVariables;
 	}
@@ -279,8 +272,7 @@ public Optional<Collation> getCollation() {
 	 * @return the mapped action or {@literal null} if the action maps to inline output.
 	 * @since 2.0.10
 	 */
-	@Nullable
-	public MapReduceAction getMapReduceAction() {
+	public @Nullable MapReduceAction getMapReduceAction() {
 		return mapReduceAction;
 	}
 
@@ -336,7 +328,6 @@ protected Document createOutObject() {
 		}
 
 		outputDatabase.ifPresent(val -> out.append("db", val));
-		outputSharded.ifPresent(val -> out.append("sharded", val));
 
 		return out;
 	}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceResults.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceResults.java
index 865a4e9438..1d4f644bd1 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceResults.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceResults.java
@@ -19,7 +19,7 @@
 import java.util.List;
 
 import org.bson.Document;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.Assert;
 
 /**
@@ -71,13 +71,11 @@ public MapReduceCounts getCounts() {
 		return mapReduceCounts;
 	}
 
-	@Nullable
-	public String getOutputCollection() {
+	public @Nullable String getOutputCollection() {
 		return outputCollection;
 	}
 
-	@Nullable
-	public Document getRawResults() {
+	public @Nullable Document getRawResults() {
 		return rawResults;
 	}
 
@@ -147,7 +145,8 @@ private static String parseOutputCollection(Document rawResults) {
 			return null;
 		}
 
-		return resultField instanceof Document document ? document.get("collection").toString()
+		return resultField instanceof Document document && document.containsKey("collection")
+				? document.get("collection").toString()
 				: resultField.toString();
 	}
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceTiming.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceTiming.java
index 28de7fe850..d99f6d9237 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceTiming.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceTiming.java
@@ -15,7 +15,7 @@
  */
 package org.springframework.data.mongodb.core.mapreduce;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /**
  * @deprecated since 3.4 in favor of {@link org.springframework.data.mongodb.core.aggregation}.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/package-info.java
index 65522d8613..c5f5840e6b 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/package-info.java
@@ -3,6 +3,6 @@
  * @deprecated since MongoDB server version 5.0
  */
 @Deprecated
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 package org.springframework.data.mongodb.core.mapreduce;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamRequest.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamRequest.java
index fec7fa60ef..e1da0b33ce 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamRequest.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamRequest.java
@@ -20,12 +20,13 @@
 
 import org.bson.BsonValue;
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.ChangeStreamOptions;
 import org.springframework.data.mongodb.core.ChangeStreamOptions.ChangeStreamOptionsBuilder;
 import org.springframework.data.mongodb.core.aggregation.Aggregation;
 import org.springframework.data.mongodb.core.messaging.ChangeStreamRequest.ChangeStreamRequestOptions;
 import org.springframework.data.mongodb.core.query.Collation;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 
 import com.mongodb.client.model.changestream.ChangeStreamDocument;
@@ -215,12 +216,12 @@ public ChangeStreamOptions getChangeStreamOptions() {
 		}
 
 		@Override
-		public String getCollectionName() {
+		public @Nullable String getCollectionName() {
 			return collectionName;
 		}
 
 		@Override
-		public String getDatabaseName() {
+		public @Nullable String getDatabaseName() {
 			return databaseName;
 		}
 
@@ -253,6 +254,7 @@ private ChangeStreamRequestBuilder() {}
 		 * @param databaseName must not be {@literal null} nor empty.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public ChangeStreamRequestBuilder<T> database(String databaseName) {
 
 			Assert.hasText(databaseName, "DatabaseName must not be null");
@@ -267,6 +269,7 @@ public ChangeStreamRequestBuilder<T> database(String databaseName) {
 		 * @param collectionName must not be {@literal null} nor empty.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public ChangeStreamRequestBuilder<T> collection(String collectionName) {
 
 			Assert.hasText(collectionName, "CollectionName must not be null");
@@ -281,6 +284,7 @@ public ChangeStreamRequestBuilder<T> collection(String collectionName) {
 		 * @param messageListener must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public ChangeStreamRequestBuilder<T> publishTo(
 				MessageListener<ChangeStreamDocument<Document>, ? super T> messageListener) {
 
@@ -308,6 +312,7 @@ public ChangeStreamRequestBuilder<T> publishTo(
 		 * @see ChangeStreamOptions#getFilter()
 		 * @see ChangeStreamOptionsBuilder#filter(Aggregation)
 		 */
+		@Contract("_ -> this")
 		public ChangeStreamRequestBuilder<T> filter(Aggregation aggregation) {
 
 			Assert.notNull(aggregation, "Aggregation must not be null");
@@ -323,6 +328,7 @@ public ChangeStreamRequestBuilder<T> filter(Aggregation aggregation) {
 		 * @return this.
 		 * @see ChangeStreamOptions#getFilter()
 		 */
+		@Contract("_ -> this")
 		public ChangeStreamRequestBuilder<T> filter(Document... pipeline) {
 
 			Assert.notNull(pipeline, "Aggregation pipeline must not be null");
@@ -340,6 +346,7 @@ public ChangeStreamRequestBuilder<T> filter(Document... pipeline) {
 		 * @see ChangeStreamOptions#getCollation()
 		 * @see ChangeStreamOptionsBuilder#collation(Collation)
 		 */
+		@Contract("_ -> this")
 		public ChangeStreamRequestBuilder<T> collation(Collation collation) {
 
 			Assert.notNull(collation, "Collation must not be null");
@@ -357,6 +364,7 @@ public ChangeStreamRequestBuilder<T> collation(Collation collation) {
 		 * @see ChangeStreamOptions#getResumeToken()
 		 * @see ChangeStreamOptionsBuilder#resumeToken(org.bson.BsonValue)
 		 */
+		@Contract("_ -> this")
 		public ChangeStreamRequestBuilder<T> resumeToken(BsonValue resumeToken) {
 
 			Assert.notNull(resumeToken, "Resume token not be null");
@@ -373,6 +381,7 @@ public ChangeStreamRequestBuilder<T> resumeToken(BsonValue resumeToken) {
 		 * @see ChangeStreamOptions#getResumeTimestamp()
 		 * @see ChangeStreamOptionsBuilder#resumeAt(java.time.Instant)
 		 */
+		@Contract("_ -> this")
 		public ChangeStreamRequestBuilder<T> resumeAt(Instant clusterTime) {
 
 			Assert.notNull(clusterTime, "ClusterTime must not be null");
@@ -388,6 +397,7 @@ public ChangeStreamRequestBuilder<T> resumeAt(Instant clusterTime) {
 		 * @return this.
 		 * @since 2.2
 		 */
+		@Contract("_ -> this")
 		public ChangeStreamRequestBuilder<T> resumeAfter(BsonValue resumeToken) {
 
 			Assert.notNull(resumeToken, "ResumeToken must not be null");
@@ -403,6 +413,7 @@ public ChangeStreamRequestBuilder<T> resumeAfter(BsonValue resumeToken) {
 		 * @return this.
 		 * @since 2.2
 		 */
+		@Contract("_ -> this")
 		public ChangeStreamRequestBuilder<T> startAfter(BsonValue resumeToken) {
 
 			Assert.notNull(resumeToken, "ResumeToken must not be null");
@@ -418,6 +429,7 @@ public ChangeStreamRequestBuilder<T> startAfter(BsonValue resumeToken) {
 		 * @see ChangeStreamOptions#getFullDocumentLookup()
 		 * @see ChangeStreamOptionsBuilder#fullDocumentLookup(FullDocument)
 		 */
+		@Contract("_ -> this")
 		public ChangeStreamRequestBuilder<T> fullDocumentLookup(FullDocument lookup) {
 
 			Assert.notNull(lookup, "FullDocument not be null");
@@ -434,6 +446,7 @@ public ChangeStreamRequestBuilder<T> fullDocumentLookup(FullDocument lookup) {
 		 * @see ChangeStreamOptions#getFullDocumentBeforeChangeLookup()
 		 * @see ChangeStreamOptionsBuilder#fullDocumentBeforeChangeLookup(FullDocumentBeforeChange)
 		 */
+		@Contract("_ -> this")
 		public ChangeStreamRequestBuilder<T> fullDocumentBeforeChangeLookup(FullDocumentBeforeChange lookup) {
 
 			Assert.notNull(lookup, "FullDocumentBeforeChange not be null");
@@ -448,6 +461,7 @@ public ChangeStreamRequestBuilder<T> fullDocumentBeforeChangeLookup(FullDocument
 		 * @param timeout must not be {@literal null}.
 		 * @since 3.0
 		 */
+		@Contract("_ -> this")
 		public ChangeStreamRequestBuilder<T> maxAwaitTime(Duration timeout) {
 
 			Assert.notNull(timeout, "timeout not be null");
@@ -459,6 +473,7 @@ public ChangeStreamRequestBuilder<T> maxAwaitTime(Duration timeout) {
 		/**
 		 * @return the build {@link ChangeStreamRequest}.
 		 */
+		@Contract("-> new")
 		public ChangeStreamRequest<T> build() {
 
 			Assert.notNull(listener, "MessageListener must not be null");
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamTask.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamTask.java
index fc8372613b..cc4d3f0bdb 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamTask.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamTask.java
@@ -27,6 +27,7 @@
 import org.bson.BsonTimestamp;
 import org.bson.BsonValue;
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.ChangeStreamEvent;
 import org.springframework.data.mongodb.core.ChangeStreamOptions;
 import org.springframework.data.mongodb.core.MongoTemplate;
@@ -39,7 +40,6 @@
 import org.springframework.data.mongodb.core.convert.QueryMapper;
 import org.springframework.data.mongodb.core.messaging.Message.MessageProperties;
 import org.springframework.data.mongodb.core.messaging.SubscriptionRequest.RequestOptions;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.ErrorHandler;
 import org.springframework.util.StringUtils;
@@ -224,21 +224,18 @@ static class ChangeStreamEventMessage<T> implements Message<ChangeStreamDocument
 			this.messageProperties = messageProperties;
 		}
 
-		@Nullable
 		@Override
-		public ChangeStreamDocument<Document> getRaw() {
+		public @Nullable ChangeStreamDocument<Document> getRaw() {
 			return delegate.getRaw();
 		}
 
-		@Nullable
 		@Override
-		public T getBody() {
+		public @Nullable T getBody() {
 			return delegate.getBody();
 		}
 
-		@Nullable
 		@Override
-		public T getBodyBeforeChange() {
+		public @Nullable T getBodyBeforeChange() {
 			return delegate.getBodyBeforeChange();
 		}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/CursorReadingTask.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/CursorReadingTask.java
index 41b5fed4f5..662960284d 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/CursorReadingTask.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/CursorReadingTask.java
@@ -21,12 +21,12 @@
 import java.util.concurrent.locks.ReentrantLock;
 import java.util.function.Supplier;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.dao.DataAccessResourceFailureException;
 import org.springframework.data.mongodb.core.MongoTemplate;
 import org.springframework.data.mongodb.core.messaging.Message.MessageProperties;
 import org.springframework.data.mongodb.core.messaging.SubscriptionRequest.RequestOptions;
 import org.springframework.data.util.Lock;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ErrorHandler;
 
@@ -51,7 +51,7 @@ abstract class CursorReadingTask<T, R> implements Task {
 
 	private State state = State.CREATED;
 
-	private MongoCursor<T> cursor;
+	private @Nullable MongoCursor<T> cursor;
 
 	/**
 	 * @param template must not be {@literal null}.
@@ -109,6 +109,7 @@ public void run() {
 	 * is immediately {@link MongoCursor#close() closed} and a new {@link MongoCursor} is requested until a valid one is
 	 * retrieved or the {@link #state} changes.
 	 */
+	@SuppressWarnings("NullAway")
 	private void start() {
 
 		lock.executeWithoutResult(() -> {
@@ -188,6 +189,7 @@ public boolean awaitStart(Duration timeout) throws InterruptedException {
 		return awaitStart.await(timeout.toNanos(), TimeUnit.NANOSECONDS);
 	}
 
+	@SuppressWarnings("NullAway")
 	protected Message<T, R> createMessage(T source, Class<R> targetType, RequestOptions options) {
 
 		SimpleMessage<T, T> message = new SimpleMessage<>(source, source, MessageProperties.builder()
@@ -209,11 +211,10 @@ private void emitMessage(Message<T, R> message) {
 		}
 	}
 
-	@Nullable
-	private T getNext() {
+	private @Nullable T getNext() {
 
 		return lock.execute(() -> {
-			if (State.RUNNING.equals(state)) {
+			if (cursor != null && State.RUNNING.equals(state)) {
 				return cursor.tryNext();
 			}
 			throw new IllegalStateException(String.format("Cursor %s is not longer open", cursor));
@@ -239,8 +240,7 @@ private static boolean isValidCursor(@Nullable MongoCursor<?> cursor) {
 	 * @return can be {@literal null}.
 	 * @throws RuntimeException The potentially translated exception.
 	 */
-	@Nullable
-	private <V> V execute(Supplier<V> callback) {
+	private <V> @Nullable V execute(Supplier<V> callback) {
 
 		try {
 			return callback.get();
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/DefaultMessageListenerContainer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/DefaultMessageListenerContainer.java
index 546f3fdd33..1b24e67e07 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/DefaultMessageListenerContainer.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/DefaultMessageListenerContainer.java
@@ -25,12 +25,12 @@
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
+import org.jspecify.annotations.Nullable;
 import org.springframework.core.task.SimpleAsyncTaskExecutor;
 import org.springframework.dao.DataAccessResourceFailureException;
 import org.springframework.data.mongodb.core.MongoTemplate;
 import org.springframework.data.mongodb.core.messaging.SubscriptionRequest.RequestOptions;
 import org.springframework.data.util.Lock;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ErrorHandler;
 import org.springframework.util.ObjectUtils;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/LazyMappingDelegatingMessage.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/LazyMappingDelegatingMessage.java
index 1c934e8302..f9a9c4131d 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/LazyMappingDelegatingMessage.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/LazyMappingDelegatingMessage.java
@@ -16,6 +16,7 @@
 package org.springframework.data.mongodb.core.messaging;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.convert.MongoConverter;
 import org.springframework.util.ClassUtils;
 
@@ -38,12 +39,12 @@ class LazyMappingDelegatingMessage<S, T> implements Message<S, T> {
 	}
 
 	@Override
-	public S getRaw() {
+	public @Nullable S getRaw() {
 		return delegate.getRaw();
 	}
 
 	@Override
-	public T getBody() {
+	public @Nullable T getBody() {
 
 		if (delegate.getBody() == null || targetType.equals(delegate.getBody().getClass())) {
 			return targetType.cast(delegate.getBody());
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/Message.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/Message.java
index 46db068096..e7aa5b036d 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/Message.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/Message.java
@@ -15,7 +15,8 @@
  */
 package org.springframework.data.mongodb.core.messaging;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 
@@ -59,8 +60,7 @@ public interface Message<S, T> {
 	 * @return can be {@literal null}.
 	 * @since 4.0
 	 */
-	@Nullable
-	default T getBodyBeforeChange() {
+	default @Nullable T getBodyBeforeChange() {
 		return null;
 	}
 
@@ -87,8 +87,7 @@ class MessageProperties {
 		 *
 		 * @return can be {@literal null}.
 		 */
-		@Nullable
-		public String getDatabaseName() {
+		public @Nullable String getDatabaseName() {
 			return databaseName;
 		}
 
@@ -97,8 +96,7 @@ public String getDatabaseName() {
 		 *
 		 * @return can be {@literal null}.
 		 */
-		@Nullable
-		public String getCollectionName() {
+		public @Nullable String getCollectionName() {
 			return collectionName;
 		}
 
@@ -162,6 +160,7 @@ public static class MessagePropertiesBuilder {
 			 * @param dbName must not be {@literal null}.
 			 * @return this.
 			 */
+			@Contract("_ -> this")
 			public MessagePropertiesBuilder databaseName(String dbName) {
 
 				Assert.notNull(dbName, "Database name must not be null");
@@ -174,6 +173,7 @@ public MessagePropertiesBuilder databaseName(String dbName) {
 			 * @param collectionName must not be {@literal null}.
 			 * @return this
 			 */
+			@Contract("_ -> this")
 			public MessagePropertiesBuilder collectionName(String collectionName) {
 
 				Assert.notNull(collectionName, "Collection name must not be null");
@@ -185,6 +185,7 @@ public MessagePropertiesBuilder collectionName(String collectionName) {
 			/**
 			 * @return the built {@link MessageProperties}.
 			 */
+			@Contract("-> new")
 			public MessageProperties build() {
 
 				MessageProperties properties = new MessageProperties();
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/SimpleMessage.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/SimpleMessage.java
index be5308e3cf..acb7bfd8a2 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/SimpleMessage.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/SimpleMessage.java
@@ -15,7 +15,7 @@
  */
 package org.springframework.data.mongodb.core.messaging;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 
@@ -46,12 +46,12 @@ class SimpleMessage<S, T> implements Message<S, T> {
 	}
 
 	@Override
-	public S getRaw() {
+	public @Nullable S getRaw() {
 		return raw;
 	}
 
 	@Override
-	public T getBody() {
+	public @Nullable T getBody() {
 		return body;
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/SubscriptionRequest.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/SubscriptionRequest.java
index 287ba293b6..7b914f16f5 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/SubscriptionRequest.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/SubscriptionRequest.java
@@ -17,9 +17,9 @@
 
 import java.time.Duration;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.MongoDatabaseFactory;
 import org.springframework.data.mongodb.core.messaging.SubscriptionRequest.RequestOptions;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
 /**
@@ -61,8 +61,7 @@ interface RequestOptions {
 		 * @return the name of the database to subscribe to. Can be {@literal null} in which case the default
 		 *         {@link MongoDatabaseFactory#getMongoDatabase() database} is used.
 		 */
-		@Nullable
-		default String getDatabaseName() {
+		default @Nullable String getDatabaseName() {
 			return null;
 		}
 
@@ -106,7 +105,7 @@ static RequestOptions justDatabase(String database) {
 			return new RequestOptions() {
 
 				@Override
-				public String getCollectionName() {
+				public @Nullable String getCollectionName() {
 					return null;
 				}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/TailableCursorRequest.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/TailableCursorRequest.java
index c6caef12fb..92e23ff847 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/TailableCursorRequest.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/TailableCursorRequest.java
@@ -18,10 +18,11 @@
 import java.util.Optional;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.messaging.SubscriptionRequest.RequestOptions;
 import org.springframework.data.mongodb.core.messaging.TailableCursorRequest.TailableCursorRequestOptions.TailableCursorRequestOptionsBuilder;
 import org.springframework.data.mongodb.core.query.Query;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 
 /**
@@ -121,6 +122,7 @@ public static class TailableCursorRequestOptions implements SubscriptionRequest.
 
 		TailableCursorRequestOptions() {}
 
+		@SuppressWarnings("NullAway")
 		public static TailableCursorRequestOptions of(RequestOptions options) {
 			return builder().collection(options.getCollectionName()).build();
 		}
@@ -136,7 +138,7 @@ public static TailableCursorRequestOptionsBuilder builder() {
 		}
 
 		@Override
-		public String getCollectionName() {
+		public @Nullable String getCollectionName() {
 			return collectionName;
 		}
 
@@ -163,6 +165,7 @@ private TailableCursorRequestOptionsBuilder() {}
 			 * @param collection must not be {@literal null} nor {@literal empty}.
 			 * @return this.
 			 */
+			@Contract("_ -> this")
 			public TailableCursorRequestOptionsBuilder collection(String collection) {
 
 				Assert.hasText(collection, "Collection must not be null nor empty");
@@ -177,6 +180,7 @@ public TailableCursorRequestOptionsBuilder collection(String collection) {
 			 * @param filter the {@link Query } to apply for filtering events. Must not be {@literal null}.
 			 * @return this.
 			 */
+			@Contract("_ -> this")
 			public TailableCursorRequestOptionsBuilder filter(Query filter) {
 
 				Assert.notNull(filter, "Filter must not be null");
@@ -188,6 +192,7 @@ public TailableCursorRequestOptionsBuilder filter(Query filter) {
 			/**
 			 * @return the built {@link TailableCursorRequestOptions}.
 			 */
+			@Contract("-> new")
 			public TailableCursorRequestOptions build() {
 
 				TailableCursorRequestOptions options = new TailableCursorRequestOptions();
@@ -220,6 +225,7 @@ private TailableCursorRequestBuilder() {}
 		 * @param collectionName must not be {@literal null} nor empty.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public TailableCursorRequestBuilder<T> collection(String collectionName) {
 
 			Assert.hasText(collectionName, "CollectionName must not be null");
@@ -234,6 +240,7 @@ public TailableCursorRequestBuilder<T> collection(String collectionName) {
 		 * @param messageListener must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public TailableCursorRequestBuilder<T> publishTo(MessageListener<Document, ? super T> messageListener) {
 
 			Assert.notNull(messageListener, "MessageListener must not be null");
@@ -248,6 +255,7 @@ public TailableCursorRequestBuilder<T> publishTo(MessageListener<Document, ? sup
 		 * @param filter the {@link Query } to apply for filtering events. Must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public TailableCursorRequestBuilder<T> filter(Query filter) {
 
 			Assert.notNull(filter, "Filter must not be null");
@@ -259,6 +267,7 @@ public TailableCursorRequestBuilder<T> filter(Query filter) {
 		/**
 		 * @return the build {@link ChangeStreamRequest}.
 		 */
+		@Contract("_ -> new")
 		public TailableCursorRequest<T> build() {
 
 			Assert.notNull(listener, "MessageListener must not be null");
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/package-info.java
index 35be8f2ef8..aa879cc3c3 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/package-info.java
@@ -2,5 +2,5 @@
  * MongoDB specific messaging support for listening to eg.
  * <a href="https://docs.mongodb.com/manual/changeStreams/">Change Streams</a>.
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 package org.springframework.data.mongodb.core.messaging;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/package-info.java
index e2f9169d0d..cae1d3df48 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/package-info.java
@@ -1,6 +1,6 @@
 /**
  * MongoDB core support.
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 package org.springframework.data.mongodb.core;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicQuery.java
index 8b1620b320..fd81030275 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicQuery.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicQuery.java
@@ -18,7 +18,8 @@
 import static org.springframework.util.ObjectUtils.*;
 
 import org.bson.Document;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 
 /**
@@ -91,6 +92,7 @@ public BasicQuery(Document queryObject, Document fieldsObject) {
 	 * @param query the query to copy.
 	 * @since 4.4
 	 */
+	@SuppressWarnings("NullAway")
 	public BasicQuery(Query query) {
 
 		super(query);
@@ -101,6 +103,7 @@ public BasicQuery(Query query) {
 	}
 
 	@Override
+	@Contract("_ -> this")
 	public Query addCriteria(CriteriaDefinition criteria) {
 
 		this.queryObject.putAll(criteria.getCriteriaObject());
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicUpdate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicUpdate.java
index 12843ce622..3d89f1e1b7 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicUpdate.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicUpdate.java
@@ -23,13 +23,11 @@
 import java.util.function.BiFunction;
 
 import org.bson.Document;
-
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.ClassUtils;
 
 /**
- * {@link Document}-based {@link Update} variant.
- *
  * @author Thomas Risberg
  * @author John Brisbin
  * @author Oliver Gierke
@@ -49,48 +47,56 @@ public BasicUpdate(Document updateObject) {
 	}
 
 	@Override
+	@Contract("_, _ -> this")
 	public Update set(String key, @Nullable Object value) {
 		setOperationValue("$set", key, value);
 		return this;
 	}
 
 	@Override
+	@Contract("_ -> this")
 	public Update unset(String key) {
 		setOperationValue("$unset", key, 1);
 		return this;
 	}
 
 	@Override
+	@Contract("_, _ -> this")
 	public Update inc(String key, Number inc) {
 		setOperationValue("$inc", key, inc);
 		return this;
 	}
 
 	@Override
+	@Contract("_, _ -> this")
 	public Update push(String key, @Nullable Object value) {
 		setOperationValue("$push", key, value);
 		return this;
 	}
 
 	@Override
+	@Contract("_, _ -> this")
 	public Update addToSet(String key, @Nullable Object value) {
 		setOperationValue("$addToSet", key, value);
 		return this;
 	}
 
 	@Override
+	@Contract("_, _ -> this")
 	public Update pop(String key, Position pos) {
 		setOperationValue("$pop", key, (pos == Position.FIRST ? -1 : 1));
 		return this;
 	}
 
 	@Override
+	@Contract("_, _ -> this")
 	public Update pull(String key, @Nullable Object value) {
 		setOperationValue("$pull", key, value);
 		return this;
 	}
 
 	@Override
+	@Contract("_, _ -> this")
 	public Update pullAll(String key, Object[] values) {
 		setOperationValue("$pullAll", key, List.of(values), (o, o2) -> {
 
@@ -107,6 +113,7 @@ public Update pullAll(String key, Object[] values) {
 	}
 
 	@Override
+	@Contract("_, _ -> this")
 	public Update rename(String oldName, String newName) {
 		setOperationValue("$rename", oldName, newName);
 		return this;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Collation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Collation.java
index de24c0511d..217e669883 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Collation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Collation.java
@@ -19,8 +19,9 @@
 import java.util.Optional;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.core.convert.converter.Converter;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 
@@ -180,6 +181,7 @@ public static Collation from(Document source) {
 	 * @param strength comparison level.
 	 * @return new {@link Collation}.
 	 */
+	@Contract("_ -> new")
 	public Collation strength(int strength) {
 
 		ComparisonLevel current = this.strength.orElseGet(() -> new ICUComparisonLevel(strength));
@@ -192,6 +194,7 @@ public Collation strength(int strength) {
 	 * @param comparisonLevel must not be {@literal null}.
 	 * @return new {@link Collation}
 	 */
+	@Contract("_ -> new")
 	public Collation strength(ComparisonLevel comparisonLevel) {
 
 		Collation newInstance = copy();
@@ -205,6 +208,7 @@ public Collation strength(ComparisonLevel comparisonLevel) {
 	 * @param caseLevel use {@literal true} to enable {@code caseLevel} comparison.
 	 * @return new {@link Collation}.
 	 */
+	@Contract("_ -> new")
 	public Collation caseLevel(boolean caseLevel) {
 
 		ComparisonLevel strengthValue = strength.orElseGet(ComparisonLevel::primary);
@@ -218,6 +222,7 @@ public Collation caseLevel(boolean caseLevel) {
 	 * @param caseFirst must not be {@literal null}.
 	 * @return new instance of {@link Collation}.
 	 */
+	@Contract("_ -> new")
 	public Collation caseFirst(String caseFirst) {
 		return caseFirst(new CaseFirst(caseFirst));
 	}
@@ -228,6 +233,7 @@ public Collation caseFirst(String caseFirst) {
 	 * @param sort must not be {@literal null}.
 	 * @return new instance of {@link Collation}.
 	 */
+	@Contract("_ -> new")
 	public Collation caseFirst(CaseFirst sort) {
 
 		ComparisonLevel strengthValue = strength.orElseGet(ComparisonLevel::tertiary);
@@ -239,6 +245,7 @@ public Collation caseFirst(CaseFirst sort) {
 	 *
 	 * @return new {@link Collation}.
 	 */
+	@Contract("-> new")
 	public Collation numericOrderingEnabled() {
 		return numericOrdering(true);
 	}
@@ -248,6 +255,7 @@ public Collation numericOrderingEnabled() {
 	 *
 	 * @return new {@link Collation}.
 	 */
+	@Contract("-> new")
 	public Collation numericOrderingDisabled() {
 		return numericOrdering(false);
 	}
@@ -257,6 +265,7 @@ public Collation numericOrderingDisabled() {
 	 *
 	 * @return new {@link Collation}.
 	 */
+	@Contract("_ -> new")
 	public Collation numericOrdering(boolean flag) {
 
 		Collation newInstance = copy();
@@ -271,6 +280,7 @@ public Collation numericOrdering(boolean flag) {
 	 * @param alternate must not be {@literal null}.
 	 * @return new {@link Collation}.
 	 */
+	@Contract("_ -> new")
 	public Collation alternate(String alternate) {
 
 		Alternate instance = this.alternate.orElseGet(() -> new Alternate(alternate, Optional.empty()));
@@ -284,6 +294,7 @@ public Collation alternate(String alternate) {
 	 * @param alternate must not be {@literal null}.
 	 * @return new {@link Collation}.
 	 */
+	@Contract("_ -> new")
 	public Collation alternate(Alternate alternate) {
 
 		Collation newInstance = copy();
@@ -296,6 +307,7 @@ public Collation alternate(Alternate alternate) {
 	 *
 	 * @return new {@link Collation}.
 	 */
+	@Contract("_ -> new")
 	public Collation backwardDiacriticSort() {
 		return backwards(true);
 	}
@@ -305,6 +317,7 @@ public Collation backwardDiacriticSort() {
 	 *
 	 * @return new {@link Collation}.
 	 */
+	@Contract("-> new")
 	public Collation forwardDiacriticSort() {
 		return backwards(false);
 	}
@@ -315,6 +328,7 @@ public Collation forwardDiacriticSort() {
 	 * @param backwards must not be {@literal null}.
 	 * @return new {@link Collation}.
 	 */
+	@Contract("_ -> new")
 	public Collation backwards(boolean backwards) {
 
 		Collation newInstance = copy();
@@ -327,6 +341,7 @@ public Collation backwards(boolean backwards) {
 	 *
 	 * @return new {@link Collation}.
 	 */
+	@Contract("-> new")
 	public Collation normalizationEnabled() {
 		return normalization(true);
 	}
@@ -336,6 +351,7 @@ public Collation normalizationEnabled() {
 	 *
 	 * @return new {@link Collation}.
 	 */
+	@Contract("-> new")
 	public Collation normalizationDisabled() {
 		return normalization(false);
 	}
@@ -346,6 +362,7 @@ public Collation normalizationDisabled() {
 	 * @param normalization must not be {@literal null}.
 	 * @return new {@link Collation}.
 	 */
+	@Contract("_ -> new")
 	public Collation normalization(boolean normalization) {
 
 		Collation newInstance = copy();
@@ -359,6 +376,7 @@ public Collation normalization(boolean normalization) {
 	 * @param maxVariable must not be {@literal null}.
 	 * @return new {@link Collation}.
 	 */
+	@Contract("_ -> new")
 	public Collation maxVariable(String maxVariable) {
 
 		Alternate alternateValue = alternate.orElseGet(Alternate::shifted);
@@ -370,6 +388,7 @@ public Collation maxVariable(String maxVariable) {
 	 *
 	 * @return the native MongoDB {@link Document} representation of the {@link Collation}.
 	 */
+	@SuppressWarnings("NullAway")
 	public Document toDocument() {
 		return map(toMongoDocumentConverter());
 	}
@@ -379,7 +398,7 @@ public Document toDocument() {
 	 *
 	 * @return he native MongoDB representation of the {@link Collation}.
 	 */
-	public com.mongodb.client.model.Collation toMongoCollation() {
+	public com.mongodb.client.model.@Nullable Collation toMongoCollation() {
 		return map(toMongoCollationConverter());
 	}
 
@@ -390,7 +409,7 @@ public com.mongodb.client.model.Collation toMongoCollation() {
 	 * @param <R>
 	 * @return the converted result.
 	 */
-	public <R> R map(Converter<? super Collation, ? extends R> mapper) {
+	public <R> @Nullable R map(Converter<? super Collation, ? extends R> mapper) {
 		return mapper.convert(this);
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java
index 8d4cb703bb..d25b98ab1a 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java
@@ -15,7 +15,7 @@
  */
 package org.springframework.data.mongodb.core.query;
 
-import static org.springframework.util.ObjectUtils.*;
+import static org.springframework.util.ObjectUtils.nullSafeHashCode;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -33,6 +33,7 @@
 import org.bson.BsonType;
 import org.bson.Document;
 import org.bson.types.Binary;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.domain.Example;
 import org.springframework.data.geo.Circle;
 import org.springframework.data.geo.Point;
@@ -45,7 +46,7 @@
 import org.springframework.data.mongodb.core.schema.JsonSchemaProperty;
 import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
 import org.springframework.data.mongodb.util.RegexFlags;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.CollectionUtils;
 import org.springframework.util.ObjectUtils;
@@ -184,6 +185,7 @@ public static Criteria expr(MongoExpression expression) {
 	 *
 	 * @return new instance of {@link Criteria}.
 	 */
+	@Contract("_ -> new")
 	public Criteria and(String key) {
 		return new Criteria(this.criteriaChain, key);
 	}
@@ -194,6 +196,7 @@ public Criteria and(String key) {
 	 * @param value can be {@literal null}.
 	 * @return this.
 	 */
+	@Contract("_ -> this")
 	public Criteria is(@Nullable Object value) {
 
 		if (!NOT_SET.equals(isValue)) {
@@ -221,6 +224,7 @@ public Criteria is(@Nullable Object value) {
 	 *      Missing Fields: Equality Filter</a>
 	 * @since 3.3
 	 */
+	@Contract("_ -> this")
 	public Criteria isNull() {
 		return is(null);
 	}
@@ -237,6 +241,7 @@ public Criteria isNull() {
 	 *      Fields: Type Check</a>
 	 * @since 3.3
 	 */
+	@Contract("_ -> this")
 	public Criteria isNullValue() {
 
 		criteria.put("$type", BsonType.NULL.getValue());
@@ -254,6 +259,7 @@ private boolean lastOperatorWasNot() {
 	 * @return this.
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/ne/">MongoDB Query operator: $ne</a>
 	 */
+	@Contract("_ -> this")
 	public Criteria ne(@Nullable Object value) {
 		criteria.put("$ne", value);
 		return this;
@@ -266,6 +272,7 @@ public Criteria ne(@Nullable Object value) {
 	 * @return this.
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/lt/">MongoDB Query operator: $lt</a>
 	 */
+	@Contract("_ -> this")
 	public Criteria lt(Object value) {
 		criteria.put("$lt", value);
 		return this;
@@ -278,6 +285,7 @@ public Criteria lt(Object value) {
 	 * @return this.
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/lte/">MongoDB Query operator: $lte</a>
 	 */
+	@Contract("_ -> this")
 	public Criteria lte(Object value) {
 		criteria.put("$lte", value);
 		return this;
@@ -290,6 +298,7 @@ public Criteria lte(Object value) {
 	 * @return this.
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/gt/">MongoDB Query operator: $gt</a>
 	 */
+	@Contract("_ -> this")
 	public Criteria gt(Object value) {
 		criteria.put("$gt", value);
 		return this;
@@ -302,6 +311,7 @@ public Criteria gt(Object value) {
 	 * @return this.
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/gte/">MongoDB Query operator: $gte</a>
 	 */
+	@Contract("_ -> this")
 	public Criteria gte(Object value) {
 		criteria.put("$gte", value);
 		return this;
@@ -314,13 +324,13 @@ public Criteria gte(Object value) {
 	 * @return this.
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/in/">MongoDB Query operator: $in</a>
 	 */
-	public Criteria in(Object... values) {
+	@Contract("_ -> this")
+	public Criteria in(@Nullable Object ... values) {
 		if (values.length > 1 && values[1] instanceof Collection) {
 			throw new InvalidMongoDbApiUsageException(
 					"You can only pass in one argument of type " + values[1].getClass().getName());
 		}
-		criteria.put("$in", Arrays.asList(values));
-		return this;
+		return this.in(Arrays.asList(values));
 	}
 
 	/**
@@ -330,8 +340,15 @@ public Criteria in(Object... values) {
 	 * @return this.
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/in/">MongoDB Query operator: $in</a>
 	 */
+	@Contract("_ -> this")
 	public Criteria in(Collection<?> values) {
-		criteria.put("$in", values);
+
+		ArrayList<?> objects = new ArrayList<>(values);
+		if (objects.size() == 1 && CollectionUtils.firstElement(objects) instanceof Placeholder placeholder) {
+			criteria.put("$in", placeholder);
+		} else {
+			criteria.put("$in", objects);
+		}
 		return this;
 	}
 
@@ -342,6 +359,7 @@ public Criteria in(Collection<?> values) {
 	 * @return this.
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/nin/">MongoDB Query operator: $nin</a>
 	 */
+	@Contract("_ -> this")
 	public Criteria nin(Object... values) {
 		return nin(Arrays.asList(values));
 	}
@@ -353,8 +371,15 @@ public Criteria nin(Object... values) {
 	 * @return this.
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/nin/">MongoDB Query operator: $nin</a>
 	 */
+	@Contract("_ -> this")
 	public Criteria nin(Collection<?> values) {
-		criteria.put("$nin", values);
+
+		ArrayList<?> objects = new ArrayList<>(values);
+		if (objects.size() == 1 && CollectionUtils.firstElement(objects) instanceof Placeholder placeholder) {
+			criteria.put("$nin", placeholder);
+		} else {
+			criteria.put("$nin", objects);
+		}
 		return this;
 	}
 
@@ -366,6 +391,7 @@ public Criteria nin(Collection<?> values) {
 	 * @return this.
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/mod/">MongoDB Query operator: $mod</a>
 	 */
+	@Contract("_ -> this")
 	public Criteria mod(Number value, Number remainder) {
 		List<Object> l = new ArrayList<>(2);
 		l.add(value);
@@ -381,6 +407,7 @@ public Criteria mod(Number value, Number remainder) {
 	 * @return this.
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/all/">MongoDB Query operator: $all</a>
 	 */
+	@Contract("_ -> this")
 	public Criteria all(Object... values) {
 		return all(Arrays.asList(values));
 	}
@@ -392,6 +419,7 @@ public Criteria all(Object... values) {
 	 * @return this.
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/all/">MongoDB Query operator: $all</a>
 	 */
+	@Contract("_ -> this")
 	public Criteria all(Collection<?> values) {
 		criteria.put("$all", values);
 		return this;
@@ -404,6 +432,7 @@ public Criteria all(Collection<?> values) {
 	 * @return this.
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/size/">MongoDB Query operator: $size</a>
 	 */
+	@Contract("_ -> this")
 	public Criteria size(int size) {
 		criteria.put("$size", size);
 		return this;
@@ -416,6 +445,7 @@ public Criteria size(int size) {
 	 * @return this.
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/exists/">MongoDB Query operator: $exists</a>
 	 */
+	@Contract("_ -> this")
 	public Criteria exists(boolean value) {
 		criteria.put("$exists", value);
 		return this;
@@ -431,6 +461,7 @@ public Criteria exists(boolean value) {
 	 *      $sampleRate</a>
 	 * @since 3.3
 	 */
+	@Contract("_ -> this")
 	public Criteria sampleRate(double sampleRate) {
 
 		Assert.isTrue(sampleRate >= 0, "The sample rate must be greater than zero");
@@ -447,6 +478,7 @@ public Criteria sampleRate(double sampleRate) {
 	 * @return this.
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/type/">MongoDB Query operator: $type</a>
 	 */
+	@Contract("_ -> this")
 	public Criteria type(int typeNumber) {
 		criteria.put("$type", typeNumber);
 		return this;
@@ -460,6 +492,7 @@ public Criteria type(int typeNumber) {
 	 * @since 2.1
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/type/">MongoDB Query operator: $type</a>
 	 */
+	@Contract("_ -> this")
 	public Criteria type(Type... types) {
 
 		Assert.notNull(types, "Types must not be null");
@@ -476,6 +509,7 @@ public Criteria type(Type... types) {
 	 * @since 3.2
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/type/">MongoDB Query operator: $type</a>
 	 */
+	@Contract("_ -> this")
 	public Criteria type(Collection<Type> types) {
 
 		Assert.notNull(types, "Types must not be null");
@@ -490,6 +524,7 @@ public Criteria type(Collection<Type> types) {
 	 * @return this.
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/not/">MongoDB Query operator: $not</a>
 	 */
+	@Contract("-> this")
 	public Criteria not() {
 		return not(null);
 	}
@@ -501,6 +536,7 @@ public Criteria not() {
 	 * @return this.
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/not/">MongoDB Query operator: $not</a>
 	 */
+	@Contract("_ -> this")
 	private Criteria not(@Nullable Object value) {
 		criteria.put("$not", value);
 		return this;
@@ -513,6 +549,7 @@ private Criteria not(@Nullable Object value) {
 	 * @return this.
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/regex/">MongoDB Query operator: $regex</a>
 	 */
+	@Contract("_ -> this")
 	public Criteria regex(String regex) {
 		return regex(regex, null);
 	}
@@ -525,6 +562,7 @@ public Criteria regex(String regex) {
 	 * @return this.
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/regex/">MongoDB Query operator: $regex</a>
 	 */
+	@Contract("_, _ -> this")
 	public Criteria regex(String regex, @Nullable String options) {
 		return regex(toPattern(regex, options));
 	}
@@ -535,6 +573,7 @@ public Criteria regex(String regex, @Nullable String options) {
 	 * @param pattern must not be {@literal null}.
 	 * @return this.
 	 */
+	@Contract("_ -> this")
 	public Criteria regex(Pattern pattern) {
 
 		Assert.notNull(pattern, "Pattern must not be null");
@@ -553,6 +592,7 @@ public Criteria regex(Pattern pattern) {
 	 * @param regex must not be {@literal null}.
 	 * @return this.
 	 */
+	@Contract("_ -> this")
 	public Criteria regex(BsonRegularExpression regex) {
 
 		if (lastOperatorWasNot()) {
@@ -581,6 +621,7 @@ private Pattern toPattern(String regex, @Nullable String options) {
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/centerSphere/">MongoDB Query operator:
 	 *      $centerSphere</a>
 	 */
+	@Contract("_ -> this")
 	public Criteria withinSphere(Circle circle) {
 
 		Assert.notNull(circle, "Circle must not be null");
@@ -597,6 +638,7 @@ public Criteria withinSphere(Circle circle) {
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/geoWithin/">MongoDB Query operator:
 	 *      $geoWithin</a>
 	 */
+	@Contract("_ -> this")
 	public Criteria within(Shape shape) {
 
 		Assert.notNull(shape, "Shape must not be null");
@@ -612,6 +654,7 @@ public Criteria within(Shape shape) {
 	 * @return this.
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/near/">MongoDB Query operator: $near</a>
 	 */
+	@Contract("_ -> this")
 	public Criteria near(Point point) {
 
 		Assert.notNull(point, "Point must not be null");
@@ -629,6 +672,7 @@ public Criteria near(Point point) {
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/nearSphere/">MongoDB Query operator:
 	 *      $nearSphere</a>
 	 */
+	@Contract("_ -> this")
 	public Criteria nearSphere(Point point) {
 
 		Assert.notNull(point, "Point must not be null");
@@ -646,6 +690,7 @@ public Criteria nearSphere(Point point) {
 	 * @since 1.8
 	 */
 	@SuppressWarnings("rawtypes")
+	@Contract("_ -> this")
 	public Criteria intersects(GeoJson geoJson) {
 
 		Assert.notNull(geoJson, "GeoJson must not be null");
@@ -665,6 +710,7 @@ public Criteria intersects(GeoJson geoJson) {
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/maxDistance/">MongoDB Query operator:
 	 *      $maxDistance</a>
 	 */
+	@Contract("_ -> this")
 	public Criteria maxDistance(double maxDistance) {
 
 		if (createNearCriteriaForCommand("$near", "$maxDistance", maxDistance)
@@ -687,6 +733,7 @@ public Criteria maxDistance(double maxDistance) {
 	 * @return this.
 	 * @since 1.7
 	 */
+	@Contract("_ -> this")
 	public Criteria minDistance(double minDistance) {
 
 		if (createNearCriteriaForCommand("$near", "$minDistance", minDistance)
@@ -706,6 +753,7 @@ public Criteria minDistance(double minDistance) {
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/elemMatch/">MongoDB Query operator:
 	 *      $elemMatch</a>
 	 */
+	@Contract("_ -> this")
 	public Criteria elemMatch(Criteria criteria) {
 		this.criteria.put("$elemMatch", criteria.getCriteriaObject());
 		return this;
@@ -718,6 +766,7 @@ public Criteria elemMatch(Criteria criteria) {
 	 * @return this.
 	 * @since 1.8
 	 */
+	@Contract("_ -> this")
 	public Criteria alike(Example<?> sample) {
 
 		if (StringUtils.hasText(this.getKey())) {
@@ -745,6 +794,7 @@ public Criteria alike(Example<?> sample) {
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/jsonSchema/">MongoDB Query operator:
 	 *      $jsonSchema</a>
 	 */
+	@Contract("_ -> this")
 	public Criteria andDocumentStructureMatches(MongoJsonSchema schema) {
 
 		Assert.notNull(schema, "Schema must not be null");
@@ -776,6 +826,7 @@ public BitwiseCriteriaOperators bits() {
 	 * @param criteria must not be {@literal null}.
 	 * @return this.
 	 */
+	@Contract("_ -> this")
 	public Criteria orOperator(Criteria... criteria) {
 
 		Assert.notNull(criteria, "Criteria must not be null");
@@ -793,6 +844,7 @@ public Criteria orOperator(Criteria... criteria) {
 	 * @return this.
 	 * @since 3.2
 	 */
+	@Contract("_ -> this")
 	public Criteria orOperator(Collection<Criteria> criteria) {
 
 		Assert.notNull(criteria, "Criteria must not be null");
@@ -810,6 +862,7 @@ public Criteria orOperator(Collection<Criteria> criteria) {
 	 * @param criteria must not be {@literal null}.
 	 * @return this.
 	 */
+	@Contract("_ -> this")
 	public Criteria norOperator(Criteria... criteria) {
 
 		Assert.notNull(criteria, "Criteria must not be null");
@@ -827,6 +880,7 @@ public Criteria norOperator(Criteria... criteria) {
 	 * @return this.
 	 * @since 3.2
 	 */
+	@Contract("_ -> this")
 	public Criteria norOperator(Collection<Criteria> criteria) {
 
 		Assert.notNull(criteria, "Criteria must not be null");
@@ -844,6 +898,7 @@ public Criteria norOperator(Collection<Criteria> criteria) {
 	 * @param criteria must not be {@literal null}.
 	 * @return this.
 	 */
+	@Contract("_ -> this")
 	public Criteria andOperator(Criteria... criteria) {
 
 		Assert.notNull(criteria, "Criteria must not be null");
@@ -861,6 +916,7 @@ public Criteria andOperator(Criteria... criteria) {
 	 * @return this.
 	 * @since 3.2
 	 */
+	@Contract("_ -> this")
 	public Criteria andOperator(Collection<Criteria> criteria) {
 
 		Assert.notNull(criteria, "Criteria must not be null");
@@ -869,6 +925,19 @@ public Criteria andOperator(Collection<Criteria> criteria) {
 		return registerCriteriaChainElement(new Criteria("$and").is(bsonList));
 	}
 
+	/**
+	 * Creates a criterion using the given {@literal operator}.
+	 *
+	 * @param operator the native MongoDB operator.
+	 * @param value the operator value
+	 * @return this
+	 * @since 5.0
+	 */
+	public Criteria raw(String operator, Object value) {
+		criteria.put(operator, value);
+		return this;
+	}
+
 	private Criteria registerCriteriaChainElement(Criteria criteria) {
 
 		if (lastOperatorWasNot()) {
@@ -884,8 +953,7 @@ private Criteria registerCriteriaChainElement(Criteria criteria) {
 	 * @see org.springframework.data.mongodb.core.query.CriteriaDefinition#getKey()
 	 */
 	@Override
-	@Nullable
-	public String getKey() {
+	public @Nullable String getKey() {
 		return this.key;
 	}
 
@@ -900,7 +968,8 @@ public Document getCriteriaObject() {
 			for (Criteria c : this.criteriaChain) {
 				Document document = c.getSingleCriteriaObject();
 				for (String k : document.keySet()) {
-					setValue(criteriaObject, k, document.get(k));
+					Object o = document.get(k);
+					setValue(criteriaObject, k, o);
 				}
 			}
 			return criteriaObject;
@@ -1095,7 +1164,7 @@ private boolean isEqual(@Nullable Object left, @Nullable Object right) {
 
 		if (Collection.class.isAssignableFrom(left.getClass())) {
 
-			if (!Collection.class.isAssignableFrom(right.getClass())) {
+			if (right == null || !Collection.class.isAssignableFrom(right.getClass())) {
 				return false;
 			}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java
index c00b1d4b82..7777e5f554 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java
@@ -16,7 +16,7 @@
 package org.springframework.data.mongodb.core.query;
 
 import org.bson.Document;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /**
  * @author Oliver Gierke
@@ -40,4 +40,41 @@ public interface CriteriaDefinition {
 	@Nullable
 	String getKey();
 
+	/**
+	 * A placeholder expression used when rending queries to JSON.
+	 *
+	 * @since 5.0
+	 * @author Christoph Strobl
+	 */
+	class Placeholder {
+
+		private final Object expression;
+
+		/**
+		 * Create a new placeholder for index bindable parameter.
+		 * 
+		 * @param position the index of the parameter to bind.
+		 * @return new instance of {@link Placeholder}.
+		 */
+		public static Placeholder indexed(int position) {
+			return new Placeholder("?%s".formatted(position));
+		}
+
+		public static Placeholder placeholder(String expression) {
+			return new Placeholder(expression);
+		}
+
+		Placeholder(Object value) {
+			this.expression = value;
+		}
+
+		public Object getValue() {
+			return expression;
+		}
+
+		@Override
+		public String toString() {
+			return getValue().toString();
+		}
+	}
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Field.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Field.java
index 3540a5a836..9775fefdb0 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Field.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Field.java
@@ -22,8 +22,9 @@
 import java.util.Map.Entry;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.MongoExpression;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 
@@ -52,6 +53,7 @@ public class Field {
 	 * @param field the document field name to be included.
 	 * @return {@code this} field projection instance.
 	 */
+	@Contract("_ -> this")
 	public Field include(String field) {
 
 		Assert.notNull(field, "Key must not be null");
@@ -111,6 +113,7 @@ public FieldProjectionExpression project(MongoExpression expression) {
 	 * @return new instance of {@link FieldProjectionExpression}.
 	 * @since 3.2
 	 */
+	@Contract("_, _ -> this")
 	public Field projectAs(MongoExpression expression, String field) {
 
 		criteria.put(field, expression);
@@ -124,6 +127,7 @@ public Field projectAs(MongoExpression expression, String field) {
 	 * @return {@code this} field projection instance.
 	 * @since 3.1
 	 */
+	@Contract("_ -> this")
 	public Field include(String... fields) {
 		return include(Arrays.asList(fields));
 	}
@@ -135,6 +139,7 @@ public Field include(String... fields) {
 	 * @return {@code this} field projection instance.
 	 * @since 4.4
 	 */
+	@Contract("_ -> this")
 	public Field include(Collection<String> fields) {
 
 		Assert.notNull(fields, "Keys must not be null");
@@ -149,6 +154,7 @@ public Field include(Collection<String> fields) {
 	 * @param field the document field name to be excluded.
 	 * @return {@code this} field projection instance.
 	 */
+	@Contract("_ -> this")
 	public Field exclude(String field) {
 
 		Assert.notNull(field, "Key must not be null");
@@ -165,6 +171,7 @@ public Field exclude(String field) {
 	 * @return {@code this} field projection instance.
 	 * @since 3.1
 	 */
+	@Contract("_ -> this")
 	public Field exclude(String... fields) {
 		return exclude(Arrays.asList(fields));
 	}
@@ -176,6 +183,7 @@ public Field exclude(String... fields) {
 	 * @return {@code this} field projection instance.
 	 * @since 4.4
 	 */
+	@Contract("_ -> this")
 	public Field exclude(Collection<String> fields) {
 
 		Assert.notNull(fields, "Keys must not be null");
@@ -191,6 +199,7 @@ public Field exclude(Collection<String> fields) {
 	 * @param size the number of elements to include.
 	 * @return {@code this} field projection instance.
 	 */
+	@Contract("_, _ -> this")
 	public Field slice(String field, int size) {
 
 		Assert.notNull(field, "Key must not be null");
@@ -209,12 +218,14 @@ public Field slice(String field, int size) {
 	 * @param size the number of elements to include.
 	 * @return {@code this} field projection instance.
 	 */
+	@Contract("_, _, _ -> this")
 	public Field slice(String field, int offset, int size) {
 
 		slices.put(field, Arrays.asList(offset, size));
 		return this;
 	}
 
+	@Contract("_, _ -> this")
 	public Field elemMatch(String field, Criteria elemMatchCriteria) {
 
 		elemMatches.put(field, elemMatchCriteria);
@@ -229,6 +240,7 @@ public Field elemMatch(String field, Criteria elemMatchCriteria) {
 	 * @param value
 	 * @return {@code this} field projection instance.
 	 */
+	@Contract("_, _ -> this")
 	public Field position(String field, int value) {
 
 		Assert.hasText(field, "DocumentField must not be null or empty");
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/GeoCommand.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/GeoCommand.java
index 83417c7200..19ecd94e23 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/GeoCommand.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/GeoCommand.java
@@ -17,12 +17,12 @@
 
 import static org.springframework.util.ObjectUtils.*;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.geo.Box;
 import org.springframework.data.geo.Circle;
 import org.springframework.data.geo.Polygon;
 import org.springframework.data.geo.Shape;
 import org.springframework.data.mongodb.core.geo.Sphere;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
 /**
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Meta.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Meta.java
index 5757aa94a2..5ec4af3989 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Meta.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Meta.java
@@ -23,7 +23,7 @@
 import java.util.Map.Entry;
 import java.util.Set;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
@@ -50,8 +50,8 @@ private enum MetaKey {
 
 	private Map<String, Object> values = Collections.emptyMap();
 	private Set<CursorOption> flags = Collections.emptySet();
-	private Integer cursorBatchSize;
-	private Boolean allowDiskUse;
+	private @Nullable Integer cursorBatchSize;
+	private @Nullable Boolean allowDiskUse;
 
 	public Meta() {}
 
@@ -85,8 +85,7 @@ public boolean hasMaxTime() {
 	/**
 	 * @return {@literal null} if not set.
 	 */
-	@Nullable
-	public Long getMaxTimeMsec() {
+	public @Nullable Long getMaxTimeMsec() {
 		return getValue(MetaKey.MAX_TIME_MS.key);
 	}
 
@@ -181,8 +180,7 @@ public void setComment(String comment) {
 	 * @return {@literal null} if not set.
 	 * @since 2.1
 	 */
-	@Nullable
-	public Integer getCursorBatchSize() {
+	public @Nullable Integer getCursorBatchSize() {
 		return cursorBatchSize;
 	}
 
@@ -285,9 +283,8 @@ void setValue(String key, @Nullable Object value) {
 		this.values.put(key, value);
 	}
 
-	@Nullable
 	@SuppressWarnings("unchecked")
-	private <T> T getValue(String key) {
+	private <T> @Nullable T getValue(String key) {
 		return (T) this.values.get(key);
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MetricConversion.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MetricConversion.java
index 571bbd275c..5625de5e93 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MetricConversion.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MetricConversion.java
@@ -20,9 +20,11 @@
 import java.math.MathContext;
 import java.math.RoundingMode;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.geo.Distance;
 import org.springframework.data.geo.Metric;
 import org.springframework.data.geo.Metrics;
+import org.springframework.util.Assert;
 
 /**
  * {@link Metric} and {@link Distance} conversions using the metric system.
@@ -151,8 +153,8 @@ static ConversionMultiplierBuilder builder() {
 	 */
 	private static class ConversionMultiplierBuilder {
 
-		private Number from;
-		private Number to;
+		private @Nullable Number from;
+		private @Nullable Number to;
 
 		ConversionMultiplierBuilder() {}
 
@@ -177,6 +179,9 @@ ConversionMultiplierBuilder to(Metric to) {
 		}
 
 		ConversionMultiplier build() {
+
+			Assert.notNull(from, "[From] must be set first");
+			Assert.notNull(to, "[To] must be set first");
 			return new ConversionMultiplier(this.from, this.to);
 		}
 	}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MongoRegexCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MongoRegexCreator.java
index e26a61c61e..b37c088981 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MongoRegexCreator.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MongoRegexCreator.java
@@ -18,7 +18,7 @@
 import java.util.regex.Pattern;
 
 import org.bson.BsonRegularExpression;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /**
  * @author Christoph Strobl
@@ -80,8 +80,7 @@ public enum MatchMode {
 	 * @param matcherType the type of matching to perform
 	 * @return {@literal source} when {@literal source} or {@literal matcherType} is {@literal null}.
 	 */
-	@Nullable
-	public String toRegularExpression(@Nullable String source, @Nullable MatchMode matcherType) {
+	public @Nullable String toRegularExpression(@Nullable String source, @Nullable MatchMode matcherType) {
 
 		if (matcherType == null || source == null) {
 			return source;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java
index f0f3b0a4dc..88d7dc5c1d 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java
@@ -18,6 +18,8 @@
 import java.util.Arrays;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
+
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.geo.CustomMetric;
 import org.springframework.data.geo.Distance;
@@ -27,7 +29,7 @@
 import org.springframework.data.mongodb.core.ReadConcernAware;
 import org.springframework.data.mongodb.core.ReadPreferenceAware;
 import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 
@@ -278,6 +280,7 @@ public Metric getMetric() {
 	 * @return
 	 * @since 2.2
 	 */
+	@Contract("_ -> this")
 	public NearQuery limit(long limit) {
 		this.limit = limit;
 		return this;
@@ -289,6 +292,7 @@ public NearQuery limit(long limit) {
 	 * @param skip
 	 * @return
 	 */
+	@Contract("_ -> this")
 	public NearQuery skip(long skip) {
 		this.skip = skip;
 		return this;
@@ -300,6 +304,7 @@ public NearQuery skip(long skip) {
 	 * @param pageable must not be {@literal null}
 	 * @return
 	 */
+	@Contract("_ -> this")
 	public NearQuery with(Pageable pageable) {
 
 		Assert.notNull(pageable, "Pageable must not be 'null'");
@@ -323,8 +328,9 @@ public NearQuery with(Pageable pageable) {
 	 * @param maxDistance
 	 * @return
 	 */
+	@Contract("_ -> this")
 	public NearQuery maxDistance(double maxDistance) {
-		return maxDistance(new Distance(maxDistance, getMetric()));
+		return maxDistance(Distance.of(maxDistance, getMetric()));
 	}
 
 	/**
@@ -335,11 +341,12 @@ public NearQuery maxDistance(double maxDistance) {
 	 * @param metric must not be {@literal null}.
 	 * @return
 	 */
+	@Contract("_, _ -> this")
 	public NearQuery maxDistance(double maxDistance, Metric metric) {
 
 		Assert.notNull(metric, "Metric must not be null");
 
-		return maxDistance(new Distance(maxDistance, metric));
+		return maxDistance(Distance.of(maxDistance, metric));
 	}
 
 	/**
@@ -349,6 +356,7 @@ public NearQuery maxDistance(double maxDistance, Metric metric) {
 	 * @param distance must not be {@literal null}.
 	 * @return
 	 */
+	@Contract("_ -> this")
 	public NearQuery maxDistance(Distance distance) {
 
 		Assert.notNull(distance, "Distance must not be null");
@@ -379,8 +387,9 @@ public NearQuery maxDistance(Distance distance) {
 	 * @return
 	 * @since 1.7
 	 */
+	@Contract("_ -> this")
 	public NearQuery minDistance(double minDistance) {
-		return minDistance(new Distance(minDistance, getMetric()));
+		return minDistance(Distance.of(minDistance, getMetric()));
 	}
 
 	/**
@@ -392,11 +401,12 @@ public NearQuery minDistance(double minDistance) {
 	 * @return
 	 * @since 1.7
 	 */
+	@Contract("_, _ -> this")
 	public NearQuery minDistance(double minDistance, Metric metric) {
 
 		Assert.notNull(metric, "Metric must not be null");
 
-		return minDistance(new Distance(minDistance, metric));
+		return minDistance(Distance.of(minDistance, metric));
 	}
 
 	/**
@@ -407,6 +417,7 @@ public NearQuery minDistance(double minDistance, Metric metric) {
 	 * @return
 	 * @since 1.7
 	 */
+	@Contract("_ -> this")
 	public NearQuery minDistance(Distance distance) {
 
 		Assert.notNull(distance, "Distance must not be null");
@@ -428,8 +439,7 @@ public NearQuery minDistance(Distance distance) {
 	 *
 	 * @return
 	 */
-	@Nullable
-	public Distance getMaxDistance() {
+	public @Nullable Distance getMaxDistance() {
 		return this.maxDistance;
 	}
 
@@ -439,8 +449,7 @@ public Distance getMaxDistance() {
 	 * @return
 	 * @since 1.7
 	 */
-	@Nullable
-	public Distance getMinDistance() {
+	public @Nullable Distance getMinDistance() {
 		return this.minDistance;
 	}
 
@@ -450,6 +459,7 @@ public Distance getMinDistance() {
 	 * @param distanceMultiplier
 	 * @return
 	 */
+	@Contract("_ -> this")
 	public NearQuery distanceMultiplier(double distanceMultiplier) {
 
 		this.metric = new CustomMetric(distanceMultiplier);
@@ -462,6 +472,7 @@ public NearQuery distanceMultiplier(double distanceMultiplier) {
 	 * @param spherical
 	 * @return
 	 */
+	@Contract("_ -> this")
 	public NearQuery spherical(boolean spherical) {
 		this.spherical = spherical;
 		return this;
@@ -482,6 +493,7 @@ public boolean isSpherical() {
 	 *
 	 * @return
 	 */
+	@Contract("-> this")
 	public NearQuery inKilometers() {
 		return adaptMetric(Metrics.KILOMETERS);
 	}
@@ -492,6 +504,7 @@ public NearQuery inKilometers() {
 	 *
 	 * @return
 	 */
+	@Contract("-> this")
 	public NearQuery inMiles() {
 		return adaptMetric(Metrics.MILES);
 	}
@@ -504,6 +517,7 @@ public NearQuery inMiles() {
 	 *          passed.
 	 * @return
 	 */
+	@Contract("_ -> this")
 	public NearQuery in(@Nullable Metric metric) {
 		return adaptMetric(metric == null ? Metrics.NEUTRAL : metric);
 	}
@@ -514,6 +528,7 @@ public NearQuery in(@Nullable Metric metric) {
 	 *
 	 * @param metric
 	 */
+	@Contract("_ -> this")
 	private NearQuery adaptMetric(Metric metric) {
 
 		if (metric != Metrics.NEUTRAL) {
@@ -530,6 +545,7 @@ private NearQuery adaptMetric(Metric metric) {
 	 * @param query must not be {@literal null}.
 	 * @return
 	 */
+	@Contract("_ -> this")
 	public NearQuery query(Query query) {
 
 		Assert.notNull(query, "Cannot apply 'null' query on NearQuery");
@@ -546,8 +562,7 @@ public NearQuery query(Query query) {
 	/**
 	 * @return the number of elements to skip.
 	 */
-	@Nullable
-	public Long getSkip() {
+	public @Nullable Long getSkip() {
 		return skip;
 	}
 
@@ -557,8 +572,7 @@ public Long getSkip() {
 	 * @return the {@link Collation} if set. {@literal null} otherwise.
 	 * @since 2.2
 	 */
-	@Nullable
-	public Collation getCollation() {
+	public @Nullable Collation getCollation() {
 		return query != null ? query.getCollation().orElse(null) : null;
 	}
 
@@ -570,6 +584,7 @@ public Collation getCollation() {
 	 * @return this.
 	 * @since 4.1
 	 */
+	@Contract("_ -> this")
 	public NearQuery withReadConcern(ReadConcern readConcern) {
 
 		Assert.notNull(readConcern, "ReadConcern must not be null");
@@ -585,6 +600,7 @@ public NearQuery withReadConcern(ReadConcern readConcern) {
 	 * @return this.
 	 * @since 4.1
 	 */
+	@Contract("_ -> this")
 	public NearQuery withReadPreference(ReadPreference readPreference) {
 
 		Assert.notNull(readPreference, "ReadPreference must not be null");
@@ -596,14 +612,13 @@ public NearQuery withReadPreference(ReadPreference readPreference) {
 	 * Get the {@link ReadConcern} to use. Will return the underlying {@link #query(Query) queries}
 	 * {@link Query#getReadConcern() ReadConcern} if present or the one defined on the {@link NearQuery#readConcern}
 	 * itself.
-	 * 
+	 *
 	 * @return can be {@literal null} if none set.
 	 * @since 4.1
 	 * @see ReadConcernAware
 	 */
-	@Nullable
 	@Override
-	public ReadConcern getReadConcern() {
+	public @Nullable ReadConcern getReadConcern() {
 
 		if (query != null && query.hasReadConcern()) {
 			return query.getReadConcern();
@@ -620,9 +635,8 @@ public ReadConcern getReadConcern() {
 	 * @since 4.1
 	 * @see ReadPreferenceAware
 	 */
-	@Nullable
 	@Override
-	public ReadPreference getReadPreference() {
+	public @Nullable ReadPreference getReadPreference() {
 
 		if (query != null && query.hasReadPreference()) {
 			return query.getReadPreference();
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java
index 31c6b9069f..47ce615fe3 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java
@@ -15,8 +15,9 @@
  */
 package org.springframework.data.mongodb.core.query;
 
-import static org.springframework.data.mongodb.core.query.SerializationUtils.*;
-import static org.springframework.util.ObjectUtils.*;
+import static org.springframework.data.mongodb.core.query.SerializationUtils.serializeToJsonSafely;
+import static org.springframework.util.ObjectUtils.nullSafeEquals;
+import static org.springframework.util.ObjectUtils.nullSafeHashCode;
 
 import java.time.Duration;
 import java.util.ArrayList;
@@ -30,6 +31,7 @@
 import java.util.Set;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.domain.KeysetScrollPosition;
 import org.springframework.data.domain.Limit;
 import org.springframework.data.domain.OffsetScrollPosition;
@@ -42,7 +44,7 @@
 import org.springframework.data.mongodb.core.ReadPreferenceAware;
 import org.springframework.data.mongodb.core.query.Meta.CursorOption;
 import org.springframework.data.mongodb.util.BsonUtils;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 
 import com.mongodb.ReadConcern;
@@ -69,7 +71,7 @@ public class Query implements ReadConcernAware, ReadPreferenceAware {
 	private long skip;
 	private Limit limit = Limit.unlimited();
 
-	private KeysetScrollPosition keysetScrollPosition;
+	private @Nullable KeysetScrollPosition keysetScrollPosition;
 	private @Nullable ReadConcern readConcern;
 	private @Nullable ReadPreference readPreference;
 
@@ -123,6 +125,7 @@ public Query(CriteriaDefinition criteriaDefinition) {
 	 * @return this.
 	 * @since 1.6
 	 */
+	@Contract("_ -> this")
 	public Query addCriteria(CriteriaDefinition criteriaDefinition) {
 
 		Assert.notNull(criteriaDefinition, "CriteriaDefinition must not be null");
@@ -157,6 +160,7 @@ public Field fields() {
 	 * @param skip number of documents to skip. Use {@literal zero} or a {@literal negative} value to avoid skipping.
 	 * @return this.
 	 */
+	@Contract("_ -> this")
 	public Query skip(long skip) {
 		this.skip = skip;
 		return this;
@@ -169,6 +173,7 @@ public Query skip(long skip) {
 	 * @param limit number of documents to return. Use {@literal zero} or {@literal negative} for unlimited.
 	 * @return this.
 	 */
+	@Contract("_ -> this")
 	public Query limit(int limit) {
 		this.limit = limit > 0 ? Limit.of(limit) : Limit.unlimited();
 		return this;
@@ -181,6 +186,7 @@ public Query limit(int limit) {
 	 * @return this.
 	 * @since 4.2
 	 */
+	@Contract("_ -> this")
 	public Query limit(Limit limit) {
 
 		Assert.notNull(limit, "Limit must not be null");
@@ -202,6 +208,7 @@ public Query limit(Limit limit) {
 	 * @return this.
 	 * @see Document#parse(String)
 	 */
+	@Contract("_ -> this")
 	public Query withHint(String hint) {
 
 		Assert.hasText(hint, "Hint must not be empty or null");
@@ -216,6 +223,7 @@ public Query withHint(String hint) {
 	 * @return this.
 	 * @since 3.1
 	 */
+	@Contract("_ -> this")
 	public Query withReadConcern(ReadConcern readConcern) {
 
 		Assert.notNull(readConcern, "ReadConcern must not be null");
@@ -230,6 +238,7 @@ public Query withReadConcern(ReadConcern readConcern) {
 	 * @return this.
 	 * @since 4.1
 	 */
+	@Contract("_ -> this")
 	public Query withReadPreference(ReadPreference readPreference) {
 
 		Assert.notNull(readPreference, "ReadPreference must not be null");
@@ -243,7 +252,7 @@ public boolean hasReadConcern() {
 	}
 
 	@Override
-	public ReadConcern getReadConcern() {
+	public @Nullable ReadConcern getReadConcern() {
 		return this.readConcern;
 	}
 
@@ -253,7 +262,7 @@ public boolean hasReadPreference() {
 	}
 
 	@Override
-	public ReadPreference getReadPreference() {
+	public @Nullable ReadPreference getReadPreference() {
 
 		if (readPreference == null) {
 			return getMeta().getFlags().contains(CursorOption.SECONDARY_READS) ? ReadPreference.primaryPreferred() : null;
@@ -269,6 +278,7 @@ public ReadPreference getReadPreference() {
 	 * @return this.
 	 * @since 2.2
 	 */
+	@Contract("_ -> this")
 	public Query withHint(Document hint) {
 
 		Assert.notNull(hint, "Hint must not be null");
@@ -283,6 +293,7 @@ public Query withHint(Document hint) {
 	 * @param pageable must not be {@literal null}.
 	 * @return this.
 	 */
+	@Contract("_ -> this")
 	public Query with(Pageable pageable) {
 
 		if (pageable.isPaged()) {
@@ -299,6 +310,7 @@ public Query with(Pageable pageable) {
 	 * @param position must not be {@literal null}.
 	 * @return this.
 	 */
+	@Contract("_ -> this")
 	public Query with(ScrollPosition position) {
 
 		Assert.notNull(position, "ScrollPosition must not be null");
@@ -320,6 +332,7 @@ public Query with(ScrollPosition position) {
 	 * @param position must not be {@literal null}.
 	 * @return this.
 	 */
+	@Contract("_ -> this")
 	public Query with(OffsetScrollPosition position) {
 
 		Assert.notNull(position, "ScrollPosition must not be null");
@@ -335,6 +348,7 @@ public Query with(OffsetScrollPosition position) {
 	 * @param position must not be {@literal null}.
 	 * @return this.
 	 */
+	@Contract("_ -> this")
 	public Query with(KeysetScrollPosition position) {
 
 		Assert.notNull(position, "ScrollPosition must not be null");
@@ -349,8 +363,7 @@ public boolean hasKeyset() {
 		return keysetScrollPosition != null;
 	}
 
-	@Nullable
-	public KeysetScrollPosition getKeyset() {
+	public @Nullable KeysetScrollPosition getKeyset() {
 		return keysetScrollPosition;
 	}
 
@@ -360,6 +373,7 @@ public KeysetScrollPosition getKeyset() {
 	 * @param sort must not be {@literal null}.
 	 * @return this.
 	 */
+	@Contract("_ -> this")
 	public Query with(Sort sort) {
 
 		Assert.notNull(sort, "Sort must not be null");
@@ -393,6 +407,7 @@ public Set<Class<?>> getRestrictedTypes() {
 	 * @param additionalTypes may not be {@literal null}
 	 * @return this.
 	 */
+	@Contract("_, _ -> this")
 	public Query restrict(Class<?> type, Class<?>... additionalTypes) {
 
 		Assert.notNull(type, "Type must not be null");
@@ -518,6 +533,7 @@ public String getHint() {
 	 * @see Meta#setMaxTimeMsec(long)
 	 * @since 1.6
 	 */
+	@Contract("_ -> this")
 	public Query maxTimeMsec(long maxTimeMsec) {
 
 		meta.setMaxTimeMsec(maxTimeMsec);
@@ -530,6 +546,7 @@ public Query maxTimeMsec(long maxTimeMsec) {
 	 * @see Meta#setMaxTime(Duration)
 	 * @since 2.1
 	 */
+	@Contract("_ -> this")
 	public Query maxTime(Duration timeout) {
 
 		meta.setMaxTime(timeout);
@@ -544,6 +561,7 @@ public Query maxTime(Duration timeout) {
 	 * @see Meta#setComment(String)
 	 * @since 1.6
 	 */
+	@Contract("_ -> this")
 	public Query comment(String comment) {
 
 		meta.setComment(comment);
@@ -562,6 +580,7 @@ public Query comment(String comment) {
 	 * @see Meta#setAllowDiskUse(Boolean)
 	 * @since 3.2
 	 */
+	@Contract("_ -> this")
 	public Query allowDiskUse(boolean allowDiskUse) {
 
 		meta.setAllowDiskUse(allowDiskUse);
@@ -578,6 +597,7 @@ public Query allowDiskUse(boolean allowDiskUse) {
 	 * @see Meta#setCursorBatchSize(int)
 	 * @since 2.1
 	 */
+	@Contract("_ -> this")
 	public Query cursorBatchSize(int batchSize) {
 
 		meta.setCursorBatchSize(batchSize);
@@ -589,6 +609,7 @@ public Query cursorBatchSize(int batchSize) {
 	 * @see org.springframework.data.mongodb.core.query.Meta.CursorOption#NO_TIMEOUT
 	 * @since 1.10
 	 */
+	@Contract("-> this")
 	public Query noCursorTimeout() {
 
 		meta.addFlag(Meta.CursorOption.NO_TIMEOUT);
@@ -600,6 +621,7 @@ public Query noCursorTimeout() {
 	 * @see org.springframework.data.mongodb.core.query.Meta.CursorOption#EXHAUST
 	 * @since 1.10
 	 */
+	@Contract("-> this")
 	public Query exhaust() {
 
 		meta.addFlag(Meta.CursorOption.EXHAUST);
@@ -613,6 +635,7 @@ public Query exhaust() {
 	 * @see org.springframework.data.mongodb.core.query.Meta.CursorOption#SECONDARY_READS
 	 * @since 3.0.2
 	 */
+	@Contract("-> this")
 	public Query allowSecondaryReads() {
 
 		meta.addFlag(Meta.CursorOption.SECONDARY_READS);
@@ -624,6 +647,7 @@ public Query allowSecondaryReads() {
 	 * @see org.springframework.data.mongodb.core.query.Meta.CursorOption#PARTIAL
 	 * @since 1.10
 	 */
+	@Contract("-> this")
 	public Query partialResults() {
 
 		meta.addFlag(Meta.CursorOption.PARTIAL);
@@ -655,6 +679,7 @@ public void setMeta(Meta meta) {
 	 * @return this.
 	 * @since 2.0
 	 */
+	@Contract("_ -> this")
 	public Query collation(@Nullable Collation collation) {
 
 		this.collation = Optional.ofNullable(collation);
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/SerializationUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/SerializationUtils.java
index 11e0f7fb24..29f8adb2c6 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/SerializationUtils.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/SerializationUtils.java
@@ -23,9 +23,9 @@
 import java.util.Map;
 
 import org.bson.Document;
-
+import org.jspecify.annotations.Nullable;
 import org.springframework.core.convert.converter.Converter;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.ObjectUtils;
 
 /**
@@ -110,8 +110,8 @@ private static void toFlatMap(String currentPath, Object source, Map<String, Obj
 	 * @param value
 	 * @return the serialized value or {@literal null}.
 	 */
-	@Nullable
-	public static String serializeToJsonSafely(@Nullable Object value) {
+	@Contract("null -> null; !null -> !null")
+	public static @Nullable String serializeToJsonSafely(@Nullable Object value) {
 
 		if (value == null) {
 			return null;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Term.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Term.java
index bd6d8c3469..cc87434178 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Term.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Term.java
@@ -15,7 +15,8 @@
  */
 package org.springframework.data.mongodb.core.query;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.ObjectUtils;
 
 /**
@@ -61,6 +62,7 @@ public Term(String raw, @Nullable Type type) {
 	 *
 	 * @return
 	 */
+	@Contract("-> this")
 	public Term negate() {
 		this.negated = true;
 		return this;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/TextCriteria.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/TextCriteria.java
index e1a7d0c4d0..5cedc2e476 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/TextCriteria.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/TextCriteria.java
@@ -19,7 +19,8 @@
 import java.util.List;
 
 import org.bson.Document;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
@@ -71,7 +72,8 @@ public static TextCriteria forDefaultLanguage() {
 	 * @param language
 	 * @return
 	 */
-	public static TextCriteria forLanguage(String language) {
+	@Contract("null -> fail")
+	public static TextCriteria forLanguage(@Nullable String language) {
 
 		Assert.hasText(language, "Language must not be null or empty");
 		return new TextCriteria(language);
@@ -83,6 +85,7 @@ public static TextCriteria forLanguage(String language) {
 	 * @param words the words to match.
 	 * @return
 	 */
+	@Contract("_ -> this")
 	public TextCriteria matchingAny(String... words) {
 
 		for (String word : words) {
@@ -97,6 +100,7 @@ public TextCriteria matchingAny(String... words) {
 	 *
 	 * @param term must not be {@literal null}.
 	 */
+	@Contract("_ -> this")
 	public TextCriteria matching(Term term) {
 
 		Assert.notNull(term, "Term to add must not be null");
@@ -109,6 +113,7 @@ public TextCriteria matching(Term term) {
 	 * @param term
 	 * @return
 	 */
+	@Contract("_ -> this")
 	public TextCriteria matching(String term) {
 
 		if (StringUtils.hasText(term)) {
@@ -121,6 +126,7 @@ public TextCriteria matching(String term) {
 	 * @param term
 	 * @return
 	 */
+	@Contract("_ -> this")
 	public TextCriteria notMatching(String term) {
 
 		if (StringUtils.hasText(term)) {
@@ -133,6 +139,7 @@ public TextCriteria notMatching(String term) {
 	 * @param words
 	 * @return
 	 */
+	@Contract("_ -> this")
 	public TextCriteria notMatchingAny(String... words) {
 
 		for (String word : words) {
@@ -147,6 +154,7 @@ public TextCriteria notMatchingAny(String... words) {
 	 * @param phrase
 	 * @return
 	 */
+	@Contract("_ -> this")
 	public TextCriteria notMatchingPhrase(String phrase) {
 
 		if (StringUtils.hasText(phrase)) {
@@ -161,6 +169,7 @@ public TextCriteria notMatchingPhrase(String phrase) {
 	 * @param phrase
 	 * @return
 	 */
+	@Contract("_ -> this")
 	public TextCriteria matchingPhrase(String phrase) {
 
 		if (StringUtils.hasText(phrase)) {
@@ -176,6 +185,7 @@ public TextCriteria matchingPhrase(String phrase) {
 	 * @return never {@literal null}.
 	 * @since 1.10
 	 */
+	@Contract("_ -> this")
 	public TextCriteria caseSensitive(boolean caseSensitive) {
 
 		this.caseSensitive = caseSensitive;
@@ -189,6 +199,7 @@ public TextCriteria caseSensitive(boolean caseSensitive) {
 	 * @return never {@literal null}.
 	 * @since 1.10
 	 */
+	@Contract("_ -> this")
 	public TextCriteria diacriticSensitive(boolean diacriticSensitive) {
 
 		this.diacriticSensitive = diacriticSensitive;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/TextQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/TextQuery.java
index a6583299d6..a9f82a857f 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/TextQuery.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/TextQuery.java
@@ -19,8 +19,9 @@
 import java.util.Map.Entry;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.util.BsonUtils;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 
 /**
  * {@link Query} implementation to be used to for performing full text searches.
@@ -100,6 +101,7 @@ public static TextQuery queryText(TextCriteria criteria) {
 	 * @see TextQuery#includeScore()
 	 * @return this.
 	 */
+	@Contract("-> this")
 	public TextQuery sortByScore() {
 
 		this.sortByScoreIndex = getSortObject().size();
@@ -113,6 +115,7 @@ public TextQuery sortByScore() {
 	 *
 	 * @return this.
 	 */
+	@Contract("-> this")
 	public TextQuery includeScore() {
 
 		this.includeScore = true;
@@ -125,6 +128,7 @@ public TextQuery includeScore() {
 	 * @param fieldname must not be {@literal null}.
 	 * @return this.
 	 */
+	@Contract("_ -> this")
 	public TextQuery includeScore(String fieldname) {
 
 		setScoreFieldName(fieldname);
@@ -170,9 +174,8 @@ public Document getSortObject() {
 
 			int sortByScoreIndex = this.sortByScoreIndex;
 
-			return sortByScoreIndex != 0
-				? sortByScoreAtPosition(super.getSortObject(), sortByScoreIndex)
-				: sortByScoreAtPositionZero();
+			return sortByScoreIndex != 0 ? sortByScoreAtPosition(super.getSortObject(), sortByScoreIndex)
+					: sortByScoreAtPositionZero();
 		}
 
 		return super.getSortObject();
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/UntypedExampleMatcher.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/UntypedExampleMatcher.java
index 677575c9e4..c02425214d 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/UntypedExampleMatcher.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/UntypedExampleMatcher.java
@@ -17,8 +17,8 @@
 
 import java.util.Set;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.domain.ExampleMatcher;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ObjectUtils;
 
 /**
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java
index 32d98f5804..cfb214a5a3 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java
@@ -27,11 +27,12 @@
 import java.util.Set;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.dao.InvalidDataAccessApiUsageException;
 import org.springframework.data.domain.Sort;
 import org.springframework.data.domain.Sort.Direction;
 import org.springframework.data.domain.Sort.Order;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
@@ -114,6 +115,7 @@ public static Update fromDocument(Document object, String... exclude) {
 	 * @return this.
 	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/update/set/">MongoDB Update operator: $set</a>
 	 */
+	@Contract("_, _ -> this")
 	public Update set(String key, @Nullable Object value) {
 		addMultiFieldOperation("$set", key, value);
 		return this;
@@ -128,6 +130,7 @@ public Update set(String key, @Nullable Object value) {
 	 * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/setOnInsert/">MongoDB Update operator:
 	 *      $setOnInsert</a>
 	 */
+	@Contract("_, _ -> this")
 	public Update setOnInsert(String key, @Nullable Object value) {
 		addMultiFieldOperation("$setOnInsert", key, value);
 		return this;
@@ -140,6 +143,7 @@ public Update setOnInsert(String key, @Nullable Object value) {
 	 * @return this.
 	 * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/unset/">MongoDB Update operator: $unset</a>
 	 */
+	@Contract("_ -> this")
 	public Update unset(String key) {
 		addMultiFieldOperation("$unset", key, 1);
 		return this;
@@ -153,12 +157,14 @@ public Update unset(String key) {
 	 * @return this.
 	 * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/inc/">MongoDB Update operator: $inc</a>
 	 */
+	@Contract("_, _ -> this")
 	public Update inc(String key, Number inc) {
 		addMultiFieldOperation("$inc", key, inc);
 		return this;
 	}
 
 	@Override
+	@Contract("_ -> this")
 	public void inc(String key) {
 		inc(key, 1L);
 	}
@@ -171,6 +177,7 @@ public void inc(String key) {
 	 * @return this.
 	 * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/push/">MongoDB Update operator: $push</a>
 	 */
+	@Contract("_, _ -> this")
 	public Update push(String key, @Nullable Object value) {
 		addMultiFieldOperation("$push", key, value);
 		return this;
@@ -207,6 +214,7 @@ public PushOperatorBuilder push(String key) {
 	 * @return new instance of {@link AddToSetBuilder}.
 	 * @since 1.5
 	 */
+	@Contract("_ -> new")
 	public AddToSetBuilder addToSet(String key) {
 		return new AddToSetBuilder(key);
 	}
@@ -220,6 +228,7 @@ public AddToSetBuilder addToSet(String key) {
 	 * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/addToSet/">MongoDB Update operator:
 	 *      $addToSet</a>
 	 */
+	@Contract("_, _ -> this")
 	public Update addToSet(String key, @Nullable Object value) {
 		addMultiFieldOperation("$addToSet", key, value);
 		return this;
@@ -233,6 +242,7 @@ public Update addToSet(String key, @Nullable Object value) {
 	 * @return this.
 	 * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/pop/">MongoDB Update operator: $pop</a>
 	 */
+	@Contract("_, _ -> this")
 	public Update pop(String key, Position pos) {
 		addMultiFieldOperation("$pop", key, pos == Position.FIRST ? -1 : 1);
 		return this;
@@ -246,6 +256,7 @@ public Update pop(String key, Position pos) {
 	 * @return this.
 	 * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/pull/">MongoDB Update operator: $pull</a>
 	 */
+	@Contract("_, _ -> this")
 	public Update pull(String key, @Nullable Object value) {
 		addMultiFieldOperation("$pull", key, value);
 		return this;
@@ -260,6 +271,7 @@ public Update pull(String key, @Nullable Object value) {
 	 * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/pullAll/">MongoDB Update operator:
 	 *      $pullAll</a>
 	 */
+	@Contract("_, _ -> this")
 	public Update pullAll(String key, Object[] values) {
 		addMultiFieldOperation("$pullAll", key, Arrays.asList(values));
 		return this;
@@ -274,6 +286,7 @@ public Update pullAll(String key, Object[] values) {
 	 * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/rename/">MongoDB Update operator:
 	 *      $rename</a>
 	 */
+	@Contract("_, _ -> this")
 	public Update rename(String oldName, String newName) {
 		addMultiFieldOperation("$rename", oldName, newName);
 		return this;
@@ -288,6 +301,7 @@ public Update rename(String oldName, String newName) {
 	 * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/currentDate/">MongoDB Update operator:
 	 *      $currentDate</a>
 	 */
+	@Contract("_ -> this")
 	public Update currentDate(String key) {
 
 		addMultiFieldOperation("$currentDate", key, true);
@@ -303,6 +317,7 @@ public Update currentDate(String key) {
 	 * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/currentDate/">MongoDB Update operator:
 	 *      $currentDate</a>
 	 */
+	@Contract("_ -> this")
 	public Update currentTimestamp(String key) {
 
 		addMultiFieldOperation("$currentDate", key, new Document("$type", "timestamp"));
@@ -318,6 +333,7 @@ public Update currentTimestamp(String key) {
 	 * @since 1.7
 	 * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/mul/">MongoDB Update operator: $mul</a>
 	 */
+	@Contract("_, _ -> this")
 	public Update multiply(String key, Number multiplier) {
 
 		Assert.notNull(multiplier, "Multiplier must not be null");
@@ -335,6 +351,7 @@ public Update multiply(String key, Number multiplier) {
 	 * @see <a href="https://docs.mongodb.com/manual/reference/bson-type-comparison-order/">Comparison/Sort Order</a>
 	 * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/max/">MongoDB Update operator: $max</a>
 	 */
+	@Contract("_, _ -> this")
 	public Update max(String key, Object value) {
 
 		Assert.notNull(value, "Value for max operation must not be null");
@@ -352,6 +369,7 @@ public Update max(String key, Object value) {
 	 * @see <a href="https://docs.mongodb.com/manual/reference/bson-type-comparison-order/">Comparison/Sort Order</a>
 	 * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/min/">MongoDB Update operator: $min</a>
 	 */
+	@Contract("_, _ -> this")
 	public Update min(String key, Object value) {
 
 		Assert.notNull(value, "Value for min operation must not be null");
@@ -366,6 +384,7 @@ public Update min(String key, Object value) {
 	 * @return this.
 	 * @since 1.7
 	 */
+	@Contract("_ -> new")
 	public BitwiseOperatorBuilder bitwise(String key) {
 		return new BitwiseOperatorBuilder(this, key);
 	}
@@ -378,6 +397,7 @@ public BitwiseOperatorBuilder bitwise(String key) {
 	 * @return this.
 	 * @since 2.0
 	 */
+	@Contract("-> this")
 	public Update isolated() {
 
 		isolated = true;
@@ -392,6 +412,7 @@ public Update isolated() {
 	 * @return this.
 	 * @since 2.2
 	 */
+	@Contract("_ -> this")
 	public Update filterArray(CriteriaDefinition criteria) {
 
 		if (arrayFilters == Collections.EMPTY_LIST) {
@@ -411,6 +432,7 @@ public Update filterArray(CriteriaDefinition criteria) {
 	 * @return this.
 	 * @since 2.2
 	 */
+	@Contract("_, _ -> this")
 	public Update filterArray(String identifier, Object expression) {
 
 		if (arrayFilters == Collections.EMPTY_LIST) {
@@ -815,6 +837,7 @@ public Update each(Object... values) {
 		 * @return never {@literal null}.
 		 * @since 1.10
 		 */
+		@Contract("_ -> this")
 		public PushOperatorBuilder slice(int count) {
 
 			this.modifiers.addModifier(new Slice(count));
@@ -829,6 +852,7 @@ public PushOperatorBuilder slice(int count) {
 		 * @return never {@literal null}.
 		 * @since 1.10
 		 */
+		@Contract("_ -> this")
 		public PushOperatorBuilder sort(Direction direction) {
 
 			Assert.notNull(direction, "Direction must not be null");
@@ -844,6 +868,7 @@ public PushOperatorBuilder sort(Direction direction) {
 		 * @return never {@literal null}.
 		 * @since 1.10
 		 */
+		@Contract("_ -> this")
 		public PushOperatorBuilder sort(Sort sort) {
 
 			Assert.notNull(sort, "Sort must not be null");
@@ -859,6 +884,7 @@ public PushOperatorBuilder sort(Sort sort) {
 		 * @return never {@literal null}.
 		 * @since 1.7
 		 */
+		@Contract("_ -> this")
 		public PushOperatorBuilder atPosition(int position) {
 
 			this.modifiers.addModifier(new PositionModifier(position));
@@ -872,6 +898,7 @@ public PushOperatorBuilder atPosition(int position) {
 		 * @return never {@literal null}.
 		 * @since 1.7
 		 */
+		@Contract("_ -> this")
 		public PushOperatorBuilder atPosition(@Nullable Position position) {
 
 			if (position == null || Position.LAST.equals(position)) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/package-info.java
index d3f67790a1..7c6889e45b 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/package-info.java
@@ -1,6 +1,6 @@
 /**
  * MongoDB specific query and update support.
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 package org.springframework.data.mongodb.core.query;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/DefaultMongoJsonSchema.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/DefaultMongoJsonSchema.java
index b59c20c6b6..da77a0199f 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/DefaultMongoJsonSchema.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/DefaultMongoJsonSchema.java
@@ -16,7 +16,7 @@
 package org.springframework.data.mongodb.core.schema;
 
 import org.bson.Document;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.CollectionUtils;
 
@@ -31,8 +31,7 @@ class DefaultMongoJsonSchema implements MongoJsonSchema {
 
 	private final JsonSchemaObject root;
 
-	@Nullable //
-	private final Document encryptionMetadata;
+	private final @Nullable Document encryptionMetadata;
 
 	DefaultMongoJsonSchema(JsonSchemaObject root) {
 		this(root, null);
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java
index 29cedfd6ce..503d591d99 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java
@@ -23,8 +23,9 @@
 import java.util.UUID;
 
 import org.bson.Document;
-
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.domain.Range;
+import org.springframework.data.mongodb.core.EncryptionAlgorithms;
 import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ArrayJsonSchemaObject;
 import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.BooleanJsonSchemaObject;
 import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.DateJsonSchemaObject;
@@ -33,7 +34,7 @@
 import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject;
 import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.StringJsonSchemaObject;
 import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.TimestampJsonSchemaObject;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
@@ -97,6 +98,7 @@ public static class UntypedJsonSchemaProperty extends IdentifiableJsonSchemaProp
 		 * @return new instance of {@link StringJsonSchemaProperty}.
 		 * @see StringJsonSchemaObject#possibleValues(Collection)
 		 */
+		@Contract("_ -> new")
 		public UntypedJsonSchemaProperty possibleValues(Object... possibleValues) {
 			return possibleValues(Arrays.asList(possibleValues));
 		}
@@ -106,6 +108,7 @@ public UntypedJsonSchemaProperty possibleValues(Object... possibleValues) {
 		 * @return new instance of {@link StringJsonSchemaProperty}.
 		 * @see StringJsonSchemaObject#allOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public UntypedJsonSchemaProperty allOf(JsonSchemaObject... allOf) {
 			return allOf(new LinkedHashSet<>(Arrays.asList(allOf)));
 		}
@@ -115,6 +118,7 @@ public UntypedJsonSchemaProperty allOf(JsonSchemaObject... allOf) {
 		 * @return new instance of {@link StringJsonSchemaProperty}.
 		 * @see StringJsonSchemaObject#anyOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public UntypedJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) {
 			return anyOf(new LinkedHashSet<>(Arrays.asList(anyOf)));
 		}
@@ -124,6 +128,7 @@ public UntypedJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) {
 		 * @return new instance of {@link StringJsonSchemaProperty}.
 		 * @see StringJsonSchemaObject#oneOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public UntypedJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) {
 			return oneOf(new LinkedHashSet<>(Arrays.asList(oneOf)));
 		}
@@ -133,6 +138,7 @@ public UntypedJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) {
 		 * @return new instance of {@link StringJsonSchemaProperty}.
 		 * @see StringJsonSchemaObject#possibleValues(Collection)
 		 */
+		@Contract("_ -> new")
 		public UntypedJsonSchemaProperty possibleValues(Collection<Object> possibleValues) {
 			return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues));
 		}
@@ -142,6 +148,7 @@ public UntypedJsonSchemaProperty possibleValues(Collection<Object> possibleValue
 		 * @return new instance of {@link StringJsonSchemaProperty}.
 		 * @see StringJsonSchemaObject#allOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public UntypedJsonSchemaProperty allOf(Collection<JsonSchemaObject> allOf) {
 			return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.allOf(allOf));
 		}
@@ -151,6 +158,7 @@ public UntypedJsonSchemaProperty allOf(Collection<JsonSchemaObject> allOf) {
 		 * @return new instance of {@link StringJsonSchemaProperty}.
 		 * @see StringJsonSchemaObject#anyOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public UntypedJsonSchemaProperty anyOf(Collection<JsonSchemaObject> anyOf) {
 			return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.anyOf(anyOf));
 		}
@@ -160,6 +168,7 @@ public UntypedJsonSchemaProperty anyOf(Collection<JsonSchemaObject> anyOf) {
 		 * @return new instance of {@link StringJsonSchemaProperty}.
 		 * @see StringJsonSchemaObject#oneOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public UntypedJsonSchemaProperty oneOf(Collection<JsonSchemaObject> oneOf) {
 			return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.oneOf(oneOf));
 		}
@@ -169,6 +178,7 @@ public UntypedJsonSchemaProperty oneOf(Collection<JsonSchemaObject> oneOf) {
 		 * @return new instance of {@link StringJsonSchemaProperty}.
 		 * @see StringJsonSchemaObject#notMatch(JsonSchemaObject)
 		 */
+		@Contract("_ -> new")
 		public UntypedJsonSchemaProperty notMatch(JsonSchemaObject notMatch) {
 			return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.notMatch(notMatch));
 		}
@@ -178,6 +188,7 @@ public UntypedJsonSchemaProperty notMatch(JsonSchemaObject notMatch) {
 		 * @return new instance of {@link StringJsonSchemaProperty}.
 		 * @see StringJsonSchemaObject#description(String)
 		 */
+		@Contract("_ -> new")
 		public UntypedJsonSchemaProperty description(String description) {
 			return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
 		}
@@ -186,6 +197,7 @@ public UntypedJsonSchemaProperty description(String description) {
 		 * @return new instance of {@link StringJsonSchemaProperty}.
 		 * @see StringJsonSchemaObject#generateDescription()
 		 */
+		@Contract("_ -> new")
 		public UntypedJsonSchemaProperty generatedDescription() {
 			return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription());
 		}
@@ -213,6 +225,7 @@ public static class StringJsonSchemaProperty extends IdentifiableJsonSchemaPrope
 		 * @return new instance of {@link StringJsonSchemaProperty}.
 		 * @see StringJsonSchemaObject#minLength(int)
 		 */
+		@Contract("_ -> new")
 		public StringJsonSchemaProperty minLength(int length) {
 			return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.minLength(length));
 		}
@@ -222,6 +235,7 @@ public StringJsonSchemaProperty minLength(int length) {
 		 * @return new instance of {@link StringJsonSchemaProperty}.
 		 * @see StringJsonSchemaObject#maxLength(int)
 		 */
+		@Contract("_ -> new")
 		public StringJsonSchemaProperty maxLength(int length) {
 			return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.maxLength(length));
 		}
@@ -231,6 +245,7 @@ public StringJsonSchemaProperty maxLength(int length) {
 		 * @return new instance of {@link StringJsonSchemaProperty}.
 		 * @see StringJsonSchemaObject#matching(String)
 		 */
+		@Contract("_ -> new")
 		public StringJsonSchemaProperty matching(String pattern) {
 			return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.matching(pattern));
 		}
@@ -240,6 +255,7 @@ public StringJsonSchemaProperty matching(String pattern) {
 		 * @return new instance of {@link StringJsonSchemaProperty}.
 		 * @see StringJsonSchemaObject#possibleValues(Collection)
 		 */
+		@Contract("_ -> new")
 		public StringJsonSchemaProperty possibleValues(String... possibleValues) {
 			return possibleValues(Arrays.asList(possibleValues));
 		}
@@ -249,6 +265,7 @@ public StringJsonSchemaProperty possibleValues(String... possibleValues) {
 		 * @return new instance of {@link StringJsonSchemaProperty}.
 		 * @see StringJsonSchemaObject#allOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public StringJsonSchemaProperty allOf(JsonSchemaObject... allOf) {
 			return allOf(new LinkedHashSet<>(Arrays.asList(allOf)));
 		}
@@ -258,6 +275,7 @@ public StringJsonSchemaProperty allOf(JsonSchemaObject... allOf) {
 		 * @return new instance of {@link StringJsonSchemaProperty}.
 		 * @see StringJsonSchemaObject#anyOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public StringJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) {
 			return anyOf(new LinkedHashSet<>(Arrays.asList(anyOf)));
 		}
@@ -267,6 +285,7 @@ public StringJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) {
 		 * @return new instance of {@link StringJsonSchemaProperty}.
 		 * @see StringJsonSchemaObject#oneOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public StringJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) {
 			return oneOf(new LinkedHashSet<>(Arrays.asList(oneOf)));
 		}
@@ -276,6 +295,7 @@ public StringJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) {
 		 * @return new instance of {@link StringJsonSchemaProperty}.
 		 * @see StringJsonSchemaObject#possibleValues(Collection)
 		 */
+		@Contract("_ -> new")
 		public StringJsonSchemaProperty possibleValues(Collection<String> possibleValues) {
 			return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues));
 		}
@@ -285,6 +305,7 @@ public StringJsonSchemaProperty possibleValues(Collection<String> possibleValues
 		 * @return new instance of {@link StringJsonSchemaProperty}.
 		 * @see StringJsonSchemaObject#allOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public StringJsonSchemaProperty allOf(Collection<JsonSchemaObject> allOf) {
 			return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.allOf(allOf));
 		}
@@ -294,6 +315,7 @@ public StringJsonSchemaProperty allOf(Collection<JsonSchemaObject> allOf) {
 		 * @return new instance of {@link StringJsonSchemaProperty}.
 		 * @see StringJsonSchemaObject#anyOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public StringJsonSchemaProperty anyOf(Collection<JsonSchemaObject> anyOf) {
 			return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.anyOf(anyOf));
 		}
@@ -303,6 +325,7 @@ public StringJsonSchemaProperty anyOf(Collection<JsonSchemaObject> anyOf) {
 		 * @return new instance of {@link StringJsonSchemaProperty}.
 		 * @see StringJsonSchemaObject#oneOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public StringJsonSchemaProperty oneOf(Collection<JsonSchemaObject> oneOf) {
 			return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.oneOf(oneOf));
 		}
@@ -312,6 +335,7 @@ public StringJsonSchemaProperty oneOf(Collection<JsonSchemaObject> oneOf) {
 		 * @return new instance of {@link StringJsonSchemaProperty}.
 		 * @see StringJsonSchemaObject#notMatch(JsonSchemaObject)
 		 */
+		@Contract("_ -> new")
 		public StringJsonSchemaProperty notMatch(JsonSchemaObject notMatch) {
 			return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.notMatch(notMatch));
 		}
@@ -321,6 +345,7 @@ public StringJsonSchemaProperty notMatch(JsonSchemaObject notMatch) {
 		 * @return new instance of {@link StringJsonSchemaProperty}.
 		 * @see StringJsonSchemaObject#description(String)
 		 */
+		@Contract("_ -> new")
 		public StringJsonSchemaProperty description(String description) {
 			return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
 		}
@@ -329,6 +354,7 @@ public StringJsonSchemaProperty description(String description) {
 		 * @return new instance of {@link StringJsonSchemaProperty}.
 		 * @see StringJsonSchemaObject#generateDescription()
 		 */
+		@Contract("_ -> new")
 		public StringJsonSchemaProperty generatedDescription() {
 			return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription());
 		}
@@ -355,6 +381,7 @@ public static class ObjectJsonSchemaProperty extends IdentifiableJsonSchemaPrope
 		 * @param range must not be {@literal null}.
 		 * @return new instance of {@link ObjectJsonSchemaProperty}.
 		 */
+		@Contract("_ -> new")
 		public ObjectJsonSchemaProperty propertiesCount(Range<Integer> range) {
 			return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.propertiesCount(range));
 		}
@@ -364,6 +391,7 @@ public ObjectJsonSchemaProperty propertiesCount(Range<Integer> range) {
 		 * @return new instance of {@link ObjectJsonSchemaProperty}.
 		 * @see ObjectJsonSchemaObject#minProperties(int)
 		 */
+		@Contract("_ -> new")
 		public ObjectJsonSchemaProperty minProperties(int count) {
 			return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.minProperties(count));
 		}
@@ -373,6 +401,7 @@ public ObjectJsonSchemaProperty minProperties(int count) {
 		 * @return new instance of {@link ObjectJsonSchemaProperty}.
 		 * @see ObjectJsonSchemaObject#maxProperties(int)
 		 */
+		@Contract("_ -> new")
 		public ObjectJsonSchemaProperty maxProperties(int count) {
 			return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.maxProperties(count));
 		}
@@ -382,6 +411,7 @@ public ObjectJsonSchemaProperty maxProperties(int count) {
 		 * @return new instance of {@link ObjectJsonSchemaProperty}.
 		 * @see ObjectJsonSchemaObject#required(String...)
 		 */
+		@Contract("_ -> new")
 		public ObjectJsonSchemaProperty required(String... properties) {
 			return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.required(properties));
 		}
@@ -391,6 +421,7 @@ public ObjectJsonSchemaProperty required(String... properties) {
 		 * @return new instance of {@link ObjectJsonSchemaProperty}.
 		 * @see ObjectJsonSchemaObject#additionalProperties(boolean)
 		 */
+		@Contract("_ -> new")
 		public ObjectJsonSchemaProperty additionalProperties(boolean additionalPropertiesAllowed) {
 			return new ObjectJsonSchemaProperty(identifier,
 					jsonSchemaObjectDelegate.additionalProperties(additionalPropertiesAllowed));
@@ -401,6 +432,7 @@ public ObjectJsonSchemaProperty additionalProperties(boolean additionalPropertie
 		 * @return new instance of {@link ObjectJsonSchemaProperty}.
 		 * @see ObjectJsonSchemaObject#additionalProperties(ObjectJsonSchemaObject)
 		 */
+		@Contract("_ -> new")
 		public ObjectJsonSchemaProperty additionalProperties(ObjectJsonSchemaObject additionalProperties) {
 			return new ObjectJsonSchemaProperty(identifier,
 					jsonSchemaObjectDelegate.additionalProperties(additionalProperties));
@@ -411,6 +443,7 @@ public ObjectJsonSchemaProperty additionalProperties(ObjectJsonSchemaObject addi
 		 * @return new instance of {@link ObjectJsonSchemaProperty}.
 		 * @see ObjectJsonSchemaObject#properties(JsonSchemaProperty...)
 		 */
+		@Contract("_ -> new")
 		public ObjectJsonSchemaProperty properties(JsonSchemaProperty... properties) {
 			return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.properties(properties));
 		}
@@ -420,6 +453,7 @@ public ObjectJsonSchemaProperty properties(JsonSchemaProperty... properties) {
 		 * @return new instance of {@link ObjectJsonSchemaProperty}.
 		 * @see ObjectJsonSchemaObject#possibleValues(Collection)
 		 */
+		@Contract("_ -> new")
 		public ObjectJsonSchemaProperty possibleValues(Object... possibleValues) {
 			return possibleValues(Arrays.asList(possibleValues));
 		}
@@ -429,6 +463,7 @@ public ObjectJsonSchemaProperty possibleValues(Object... possibleValues) {
 		 * @return new instance of {@link ObjectJsonSchemaProperty}.
 		 * @see ObjectJsonSchemaObject#allOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public ObjectJsonSchemaProperty allOf(JsonSchemaObject... allOf) {
 			return allOf(new LinkedHashSet<>(Arrays.asList(allOf)));
 		}
@@ -438,6 +473,7 @@ public ObjectJsonSchemaProperty allOf(JsonSchemaObject... allOf) {
 		 * @return new instance of {@link ObjectJsonSchemaProperty}.
 		 * @see ObjectJsonSchemaObject#anyOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public ObjectJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) {
 			return anyOf(new LinkedHashSet<>(Arrays.asList(anyOf)));
 		}
@@ -447,6 +483,7 @@ public ObjectJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) {
 		 * @return new instance of {@link ObjectJsonSchemaProperty}.
 		 * @see ObjectJsonSchemaObject#oneOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public ObjectJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) {
 			return oneOf(new LinkedHashSet<>(Arrays.asList(oneOf)));
 		}
@@ -456,6 +493,7 @@ public ObjectJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) {
 		 * @return new instance of {@link ObjectJsonSchemaProperty}.
 		 * @see ObjectJsonSchemaObject#possibleValues(Collection)
 		 */
+		@Contract("_ -> new")
 		public ObjectJsonSchemaProperty possibleValues(Collection<Object> possibleValues) {
 			return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues));
 		}
@@ -465,6 +503,7 @@ public ObjectJsonSchemaProperty possibleValues(Collection<Object> possibleValues
 		 * @return new instance of {@link ObjectJsonSchemaProperty}.
 		 * @see ObjectJsonSchemaObject#allOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public ObjectJsonSchemaProperty allOf(Collection<JsonSchemaObject> allOf) {
 			return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.allOf(allOf));
 		}
@@ -474,6 +513,7 @@ public ObjectJsonSchemaProperty allOf(Collection<JsonSchemaObject> allOf) {
 		 * @return new instance of {@link ObjectJsonSchemaProperty}.
 		 * @see ObjectJsonSchemaObject#anyOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public ObjectJsonSchemaProperty anyOf(Collection<JsonSchemaObject> anyOf) {
 			return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.anyOf(anyOf));
 		}
@@ -483,6 +523,7 @@ public ObjectJsonSchemaProperty anyOf(Collection<JsonSchemaObject> anyOf) {
 		 * @return new instance of {@link ObjectJsonSchemaProperty}.
 		 * @see ObjectJsonSchemaObject#oneOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public ObjectJsonSchemaProperty oneOf(Collection<JsonSchemaObject> oneOf) {
 			return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.oneOf(oneOf));
 		}
@@ -492,6 +533,7 @@ public ObjectJsonSchemaProperty oneOf(Collection<JsonSchemaObject> oneOf) {
 		 * @return new instance of {@link ObjectJsonSchemaProperty}.
 		 * @see ObjectJsonSchemaObject#notMatch(JsonSchemaObject)
 		 */
+		@Contract("_ -> new")
 		public ObjectJsonSchemaProperty notMatch(JsonSchemaObject notMatch) {
 			return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.notMatch(notMatch));
 		}
@@ -501,6 +543,7 @@ public ObjectJsonSchemaProperty notMatch(JsonSchemaObject notMatch) {
 		 * @return new instance of {@link ObjectJsonSchemaProperty}.
 		 * @see ObjectJsonSchemaObject#description(String)
 		 */
+		@Contract("_ -> new")
 		public ObjectJsonSchemaProperty description(String description) {
 			return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
 		}
@@ -509,6 +552,7 @@ public ObjectJsonSchemaProperty description(String description) {
 		 * @return new instance of {@link ObjectJsonSchemaProperty}.
 		 * @see ObjectJsonSchemaObject#generateDescription()
 		 */
+		@Contract("_ -> new")
 		public ObjectJsonSchemaProperty generatedDescription() {
 			return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription());
 		}
@@ -540,6 +584,7 @@ public NumericJsonSchemaProperty(String identifier, NumericJsonSchemaObject sche
 		 * @return new instance of {@link NumericJsonSchemaProperty}.
 		 * @see NumericJsonSchemaObject#multipleOf
 		 */
+		@Contract("_ -> new")
 		public NumericJsonSchemaProperty multipleOf(Number value) {
 			return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.multipleOf(value));
 		}
@@ -549,6 +594,7 @@ public NumericJsonSchemaProperty multipleOf(Number value) {
 		 * @return new instance of {@link NumericJsonSchemaProperty}.
 		 * @see NumericJsonSchemaObject#within(Range)
 		 */
+		@Contract("_ -> new")
 		public NumericJsonSchemaProperty within(Range<? extends Number> range) {
 			return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.within(range));
 		}
@@ -558,6 +604,7 @@ public NumericJsonSchemaProperty within(Range<? extends Number> range) {
 		 * @return new instance of {@link NumericJsonSchemaProperty}.
 		 * @see NumericJsonSchemaObject#gt(Number)
 		 */
+		@Contract("_ -> new")
 		public NumericJsonSchemaProperty gt(Number min) {
 			return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.gt(min));
 		}
@@ -567,6 +614,7 @@ public NumericJsonSchemaProperty gt(Number min) {
 		 * @return new instance of {@link NumericJsonSchemaProperty}.
 		 * @see NumericJsonSchemaObject#gte(Number)
 		 */
+		@Contract("_ -> new")
 		public NumericJsonSchemaProperty gte(Number min) {
 			return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.gte(min));
 		}
@@ -576,6 +624,7 @@ public NumericJsonSchemaProperty gte(Number min) {
 		 * @return new instance of {@link NumericJsonSchemaProperty}.
 		 * @see NumericJsonSchemaObject#lt(Number)
 		 */
+		@Contract("_ -> new")
 		public NumericJsonSchemaProperty lt(Number max) {
 			return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.lt(max));
 		}
@@ -585,6 +634,7 @@ public NumericJsonSchemaProperty lt(Number max) {
 		 * @return new instance of {@link NumericJsonSchemaProperty}.
 		 * @see NumericJsonSchemaObject#lte(Number)
 		 */
+		@Contract("_ -> new")
 		public NumericJsonSchemaProperty lte(Number max) {
 			return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.lte(max));
 		}
@@ -594,6 +644,7 @@ public NumericJsonSchemaProperty lte(Number max) {
 		 * @return new instance of {@link NumericJsonSchemaProperty}.
 		 * @see NumericJsonSchemaObject#possibleValues(Collection)
 		 */
+		@Contract("_ -> new")
 		public NumericJsonSchemaProperty possibleValues(Number... possibleValues) {
 			return possibleValues(new LinkedHashSet<>(Arrays.asList(possibleValues)));
 		}
@@ -603,6 +654,7 @@ public NumericJsonSchemaProperty possibleValues(Number... possibleValues) {
 		 * @return new instance of {@link NumericJsonSchemaProperty}.
 		 * @see NumericJsonSchemaObject#allOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public NumericJsonSchemaProperty allOf(JsonSchemaObject... allOf) {
 			return allOf(Arrays.asList(allOf));
 		}
@@ -612,6 +664,7 @@ public NumericJsonSchemaProperty allOf(JsonSchemaObject... allOf) {
 		 * @return new instance of {@link NumericJsonSchemaProperty}.
 		 * @see NumericJsonSchemaObject#anyOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public NumericJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) {
 			return anyOf(new LinkedHashSet<>(Arrays.asList(anyOf)));
 		}
@@ -621,6 +674,7 @@ public NumericJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) {
 		 * @return new instance of {@link NumericJsonSchemaProperty}.
 		 * @see NumericJsonSchemaObject#oneOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public NumericJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) {
 			return oneOf(new LinkedHashSet<>(Arrays.asList(oneOf)));
 		}
@@ -630,6 +684,7 @@ public NumericJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) {
 		 * @return new instance of {@link NumericJsonSchemaProperty}.
 		 * @see NumericJsonSchemaObject#possibleValues(Collection)
 		 */
+		@Contract("_ -> new")
 		public NumericJsonSchemaProperty possibleValues(Collection<Number> possibleValues) {
 			return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues));
 		}
@@ -639,6 +694,7 @@ public NumericJsonSchemaProperty possibleValues(Collection<Number> possibleValue
 		 * @return new instance of {@link NumericJsonSchemaProperty}.
 		 * @see NumericJsonSchemaObject#allOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public NumericJsonSchemaProperty allOf(Collection<JsonSchemaObject> allOf) {
 			return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.allOf(allOf));
 		}
@@ -648,6 +704,7 @@ public NumericJsonSchemaProperty allOf(Collection<JsonSchemaObject> allOf) {
 		 * @return new instance of {@link NumericJsonSchemaProperty}.
 		 * @see NumericJsonSchemaObject#anyOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public NumericJsonSchemaProperty anyOf(Collection<JsonSchemaObject> anyOf) {
 			return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.anyOf(anyOf));
 		}
@@ -657,6 +714,7 @@ public NumericJsonSchemaProperty anyOf(Collection<JsonSchemaObject> anyOf) {
 		 * @return new instance of {@link NumericJsonSchemaProperty}.
 		 * @see NumericJsonSchemaObject#oneOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public NumericJsonSchemaProperty oneOf(Collection<JsonSchemaObject> oneOf) {
 			return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.oneOf(oneOf));
 		}
@@ -666,6 +724,7 @@ public NumericJsonSchemaProperty oneOf(Collection<JsonSchemaObject> oneOf) {
 		 * @return new instance of {@link NumericJsonSchemaProperty}.
 		 * @see NumericJsonSchemaObject#notMatch(JsonSchemaObject)
 		 */
+		@Contract("_ -> new")
 		public NumericJsonSchemaProperty notMatch(JsonSchemaObject notMatch) {
 			return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.notMatch(notMatch));
 		}
@@ -675,6 +734,7 @@ public NumericJsonSchemaProperty notMatch(JsonSchemaObject notMatch) {
 		 * @return new instance of {@link NumericJsonSchemaProperty}.
 		 * @see NumericJsonSchemaObject#description(String)
 		 */
+		@Contract("_ -> new")
 		public NumericJsonSchemaProperty description(String description) {
 			return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
 		}
@@ -710,6 +770,7 @@ public ArrayJsonSchemaProperty(String identifier, ArrayJsonSchemaObject schemaOb
 		 * @return new instance of {@link ArrayJsonSchemaProperty}.
 		 * @see ArrayJsonSchemaObject#uniqueItems(boolean)
 		 */
+		@Contract("_ -> new")
 		public ArrayJsonSchemaProperty uniqueItems(boolean uniqueItems) {
 			return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.uniqueItems(uniqueItems));
 		}
@@ -719,6 +780,7 @@ public ArrayJsonSchemaProperty uniqueItems(boolean uniqueItems) {
 		 * @return new instance of {@link ArrayJsonSchemaProperty}.
 		 * @see ArrayJsonSchemaObject#range(Range)
 		 */
+		@Contract("_ -> new")
 		public ArrayJsonSchemaProperty range(Range<Integer> range) {
 			return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.range(range));
 		}
@@ -728,6 +790,7 @@ public ArrayJsonSchemaProperty range(Range<Integer> range) {
 		 * @return new instance of {@link ArrayJsonSchemaProperty}.
 		 * @see ArrayJsonSchemaObject#minItems(int)
 		 */
+		@Contract("_ -> new")
 		public ArrayJsonSchemaProperty minItems(int count) {
 			return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.minItems(count));
 		}
@@ -737,6 +800,7 @@ public ArrayJsonSchemaProperty minItems(int count) {
 		 * @return new instance of {@link ArrayJsonSchemaProperty}.
 		 * @see ArrayJsonSchemaObject#maxItems(int)
 		 */
+		@Contract("_ -> new")
 		public ArrayJsonSchemaProperty maxItems(int count) {
 			return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.maxItems(count));
 		}
@@ -746,6 +810,7 @@ public ArrayJsonSchemaProperty maxItems(int count) {
 		 * @return new instance of {@link ArrayJsonSchemaProperty}.
 		 * @see ArrayJsonSchemaObject#items(Collection)
 		 */
+		@Contract("_ -> new")
 		public ArrayJsonSchemaProperty items(JsonSchemaObject... items) {
 			return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.items(Arrays.asList(items)));
 		}
@@ -755,6 +820,7 @@ public ArrayJsonSchemaProperty items(JsonSchemaObject... items) {
 		 * @return new instance of {@link ArrayJsonSchemaProperty}.
 		 * @see ArrayJsonSchemaObject#items(Collection)
 		 */
+		@Contract("_ -> new")
 		public ArrayJsonSchemaProperty items(Collection<JsonSchemaObject> items) {
 			return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.items(items));
 		}
@@ -764,6 +830,7 @@ public ArrayJsonSchemaProperty items(Collection<JsonSchemaObject> items) {
 		 * @return new instance of {@link ArrayJsonSchemaProperty}.
 		 * @see ArrayJsonSchemaObject#additionalItems(boolean)
 		 */
+		@Contract("_ -> new")
 		public ArrayJsonSchemaProperty additionalItems(boolean additionalItemsAllowed) {
 			return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.additionalItems(additionalItemsAllowed));
 		}
@@ -773,6 +840,7 @@ public ArrayJsonSchemaProperty additionalItems(boolean additionalItemsAllowed) {
 		 * @return new instance of {@link ArrayJsonSchemaProperty}.
 		 * @see ArrayJsonSchemaObject#possibleValues(Collection)
 		 */
+		@Contract("_ -> new")
 		public ArrayJsonSchemaProperty possibleValues(Object... possibleValues) {
 			return possibleValues(new LinkedHashSet<>(Arrays.asList(possibleValues)));
 		}
@@ -782,6 +850,7 @@ public ArrayJsonSchemaProperty possibleValues(Object... possibleValues) {
 		 * @return new instance of {@link ArrayJsonSchemaProperty}.
 		 * @see ArrayJsonSchemaObject#allOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public ArrayJsonSchemaProperty allOf(JsonSchemaObject... allOf) {
 			return allOf(new LinkedHashSet<>(Arrays.asList(allOf)));
 		}
@@ -791,6 +860,7 @@ public ArrayJsonSchemaProperty allOf(JsonSchemaObject... allOf) {
 		 * @return new instance of {@link ArrayJsonSchemaProperty}.
 		 * @see ArrayJsonSchemaObject#anyOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public ArrayJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) {
 			return anyOf(new LinkedHashSet<>(Arrays.asList(anyOf)));
 		}
@@ -800,6 +870,7 @@ public ArrayJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) {
 		 * @return new instance of {@link ArrayJsonSchemaProperty}.
 		 * @see ArrayJsonSchemaObject#oneOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public ArrayJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) {
 			return oneOf(new LinkedHashSet<>(Arrays.asList(oneOf)));
 		}
@@ -809,6 +880,7 @@ public ArrayJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) {
 		 * @return new instance of {@link ArrayJsonSchemaProperty}.
 		 * @see ArrayJsonSchemaObject#possibleValues(Collection)
 		 */
+		@Contract("_ -> new")
 		public ArrayJsonSchemaProperty possibleValues(Collection<Object> possibleValues) {
 			return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues));
 		}
@@ -818,6 +890,7 @@ public ArrayJsonSchemaProperty possibleValues(Collection<Object> possibleValues)
 		 * @return new instance of {@link ArrayJsonSchemaProperty}.
 		 * @see ArrayJsonSchemaObject#allOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public ArrayJsonSchemaProperty allOf(Collection<JsonSchemaObject> allOf) {
 			return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.allOf(allOf));
 		}
@@ -827,6 +900,7 @@ public ArrayJsonSchemaProperty allOf(Collection<JsonSchemaObject> allOf) {
 		 * @return new instance of {@link ArrayJsonSchemaProperty}.
 		 * @see ArrayJsonSchemaObject#anyOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public ArrayJsonSchemaProperty anyOf(Collection<JsonSchemaObject> anyOf) {
 			return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.anyOf(anyOf));
 		}
@@ -836,6 +910,7 @@ public ArrayJsonSchemaProperty anyOf(Collection<JsonSchemaObject> anyOf) {
 		 * @return new instance of {@link ArrayJsonSchemaProperty}.
 		 * @see ArrayJsonSchemaObject#oneOf(Collection)
 		 */
+		@Contract("_ -> new")
 		public ArrayJsonSchemaProperty oneOf(Collection<JsonSchemaObject> oneOf) {
 			return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.oneOf(oneOf));
 		}
@@ -845,6 +920,7 @@ public ArrayJsonSchemaProperty oneOf(Collection<JsonSchemaObject> oneOf) {
 		 * @return new instance of {@link ArrayJsonSchemaProperty}.
 		 * @see ArrayJsonSchemaObject#notMatch(JsonSchemaObject)
 		 */
+		@Contract("_ -> new")
 		public ArrayJsonSchemaProperty notMatch(JsonSchemaObject notMatch) {
 			return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.notMatch(notMatch));
 		}
@@ -854,6 +930,7 @@ public ArrayJsonSchemaProperty notMatch(JsonSchemaObject notMatch) {
 		 * @return new instance of {@link NumericJsonSchemaProperty}.
 		 * @see ArrayJsonSchemaObject#description(String)
 		 */
+		@Contract("_ -> new")
 		public ArrayJsonSchemaProperty description(String description) {
 			return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
 		}
@@ -884,6 +961,7 @@ public static class BooleanJsonSchemaProperty extends IdentifiableJsonSchemaProp
 		 * @return new instance of {@link NumericJsonSchemaProperty}.
 		 * @see BooleanJsonSchemaObject#description(String)
 		 */
+		@Contract("_ -> new")
 		public BooleanJsonSchemaProperty description(String description) {
 			return new BooleanJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
 		}
@@ -914,6 +992,7 @@ public static class NullJsonSchemaProperty extends IdentifiableJsonSchemaPropert
 		 * @return new instance of {@link NullJsonSchemaProperty}.
 		 * @see NullJsonSchemaObject#description(String)
 		 */
+		@Contract("_ -> new")
 		public NullJsonSchemaProperty description(String description) {
 			return new NullJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
 		}
@@ -944,6 +1023,7 @@ public static class DateJsonSchemaProperty extends IdentifiableJsonSchemaPropert
 		 * @return new instance of {@link DateJsonSchemaProperty}.
 		 * @see DateJsonSchemaProperty#description(String)
 		 */
+		@Contract("_ -> new")
 		public DateJsonSchemaProperty description(String description) {
 			return new DateJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
 		}
@@ -974,6 +1054,7 @@ public static class TimestampJsonSchemaProperty extends IdentifiableJsonSchemaPr
 		 * @return new instance of {@link TimestampJsonSchemaProperty}.
 		 * @see TimestampJsonSchemaProperty#description(String)
 		 */
+		@Contract("_ -> new")
 		public TimestampJsonSchemaProperty description(String description) {
 			return new TimestampJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
 		}
@@ -1036,7 +1117,7 @@ public static class EncryptedJsonSchemaProperty implements JsonSchemaProperty {
 
 		private final JsonSchemaProperty targetProperty;
 		private final @Nullable String algorithm;
-		private final @Nullable String keyId;
+		private final @Nullable Object keyId;
 		private final @Nullable List<?> keyIds;
 
 		/**
@@ -1048,7 +1129,7 @@ public EncryptedJsonSchemaProperty(JsonSchemaProperty target) {
 			this(target, null, null, null);
 		}
 
-		private EncryptedJsonSchemaProperty(JsonSchemaProperty target, @Nullable String algorithm, @Nullable String keyId,
+		private EncryptedJsonSchemaProperty(JsonSchemaProperty target, @Nullable String algorithm, @Nullable Object keyId,
 				@Nullable List<?> keyIds) {
 
 			Assert.notNull(target, "Target must not be null");
@@ -1068,13 +1149,26 @@ public static EncryptedJsonSchemaProperty encrypted(JsonSchemaProperty target) {
 			return new EncryptedJsonSchemaProperty(target);
 		}
 
+		/**
+		 * Create new instance of {@link EncryptedJsonSchemaProperty} with {@literal Range} encryption, wrapping the given
+		 * {@link JsonSchemaProperty target}.
+		 *
+		 * @param target must not be {@literal null}.
+		 * @return new instance of {@link EncryptedJsonSchemaProperty}.
+		 * @since 4.5
+		 */
+		public static EncryptedJsonSchemaProperty rangeEncrypted(JsonSchemaProperty target) {
+			return new EncryptedJsonSchemaProperty(target).algorithm(EncryptionAlgorithms.RANGE);
+		}
+
 		/**
 		 * Use {@literal AEAD_AES_256_CBC_HMAC_SHA_512-Random} algorithm.
 		 *
 		 * @return new instance of {@link EncryptedJsonSchemaProperty}.
 		 */
+		@Contract("-> new")
 		public EncryptedJsonSchemaProperty aead_aes_256_cbc_hmac_sha_512_random() {
-			return algorithm("AEAD_AES_256_CBC_HMAC_SHA_512-Random");
+			return algorithm(EncryptionAlgorithms.AEAD_AES_256_CBC_HMAC_SHA_512_Random);
 		}
 
 		/**
@@ -1082,8 +1176,9 @@ public EncryptedJsonSchemaProperty aead_aes_256_cbc_hmac_sha_512_random() {
 		 *
 		 * @return new instance of {@link EncryptedJsonSchemaProperty}.
 		 */
+		@Contract("-> new")
 		public EncryptedJsonSchemaProperty aead_aes_256_cbc_hmac_sha_512_deterministic() {
-			return algorithm("AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic");
+			return algorithm(EncryptionAlgorithms.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic);
 		}
 
 		/**
@@ -1091,6 +1186,7 @@ public EncryptedJsonSchemaProperty aead_aes_256_cbc_hmac_sha_512_deterministic()
 		 *
 		 * @return new instance of {@link EncryptedJsonSchemaProperty}.
 		 */
+		@Contract("_ -> new")
 		public EncryptedJsonSchemaProperty algorithm(String algorithm) {
 			return new EncryptedJsonSchemaProperty(targetProperty, algorithm, keyId, keyIds);
 		}
@@ -1099,14 +1195,25 @@ public EncryptedJsonSchemaProperty algorithm(String algorithm) {
 		 * @param keyId must not be {@literal null}.
 		 * @return new instance of {@link EncryptedJsonSchemaProperty}.
 		 */
+		@Contract("_ -> new")
 		public EncryptedJsonSchemaProperty keyId(String keyId) {
 			return new EncryptedJsonSchemaProperty(targetProperty, algorithm, keyId, null);
 		}
 
+		/**
+		 * @param keyId must not be {@literal null}.
+		 * @return new instance of {@link EncryptedJsonSchemaProperty}.
+		 * @since 4.5
+		 */
+		public EncryptedJsonSchemaProperty keyId(Object keyId) {
+			return new EncryptedJsonSchemaProperty(targetProperty, algorithm, keyId, null);
+		}
+
 		/**
 		 * @param keyId must not be {@literal null}.
 		 * @return new instance of {@link EncryptedJsonSchemaProperty}.
 		 */
+		@Contract("_ -> new")
 		public EncryptedJsonSchemaProperty keys(UUID... keyId) {
 			return new EncryptedJsonSchemaProperty(targetProperty, algorithm, null, Arrays.asList(keyId));
 		}
@@ -1115,6 +1222,7 @@ public EncryptedJsonSchemaProperty keys(UUID... keyId) {
 		 * @param keyId must not be {@literal null}.
 		 * @return new instance of {@link EncryptedJsonSchemaProperty}.
 		 */
+		@Contract("_ -> new")
 		public EncryptedJsonSchemaProperty keys(Object... keyId) {
 			return new EncryptedJsonSchemaProperty(targetProperty, algorithm, null, Arrays.asList(keyId));
 		}
@@ -1159,8 +1267,8 @@ public Set<Type> getTypes() {
 			return targetProperty.getTypes();
 		}
 
-		@Nullable
-		private Type extractPropertyType(Document source) {
+
+		private @Nullable Type extractPropertyType(Document source) {
 
 			if (source.containsKey("type")) {
 				return Type.of(source.get("type", String.class));
@@ -1171,5 +1279,71 @@ private Type extractPropertyType(Document source) {
 
 			return null;
 		}
+
+		public @Nullable Object getKeyId() {
+			if (keyId != null) {
+				return keyId;
+			}
+			if (keyIds != null && keyIds.size() == 1) {
+				return keyIds.iterator().next();
+			}
+			return null;
+		}
+	}
+
+	/**
+	 * {@link JsonSchemaProperty} implementation typically wrapping an {@link EncryptedJsonSchemaProperty encrypted
+	 * property} to mark it as queryable.
+	 *
+	 * @author Christoph Strobl
+	 * @since 4.5
+	 */
+	public static class QueryableJsonSchemaProperty implements JsonSchemaProperty {
+
+		private final JsonSchemaProperty targetProperty;
+		private final QueryCharacteristics characteristics;
+
+		public QueryableJsonSchemaProperty(JsonSchemaProperty target, QueryCharacteristics characteristics) {
+			this.targetProperty = target;
+			this.characteristics = characteristics;
+		}
+
+		@Override
+		public Document toDocument() {
+
+			Document doc = targetProperty.toDocument();
+			Document propertySpecification = doc.get(targetProperty.getIdentifier(), Document.class);
+
+			if (propertySpecification.containsKey("encrypt")) {
+				Document encrypt = propertySpecification.get("encrypt", Document.class);
+				List<Document> queries = characteristics.getCharacteristics().stream().map(QueryCharacteristic::toDocument)
+						.toList();
+				encrypt.append("queries", queries);
+			}
+
+			return doc;
+		}
+
+		@Override
+		public String getIdentifier() {
+			return targetProperty.getIdentifier();
+		}
+
+		@Override
+		public Set<Type> getTypes() {
+			return targetProperty.getTypes();
+		}
+
+		boolean isEncrypted() {
+			return targetProperty instanceof EncryptedJsonSchemaProperty;
+		}
+
+		public JsonSchemaProperty getTargetProperty() {
+			return targetProperty;
+		}
+
+		public QueryCharacteristics getCharacteristics() {
+			return characteristics;
+		}
 	}
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaObject.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaObject.java
index a84f361d37..24a40efa5b 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaObject.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaObject.java
@@ -31,6 +31,7 @@
 import org.bson.types.Code;
 import org.bson.types.Decimal128;
 import org.bson.types.ObjectId;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ArrayJsonSchemaObject;
 import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.BooleanJsonSchemaObject;
 import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.DateJsonSchemaObject;
@@ -39,7 +40,6 @@
 import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject;
 import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.StringJsonSchemaObject;
 import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.TimestampJsonSchemaObject;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.ObjectUtils;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaProperty.java
index 8529951db2..20d735ee03 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaProperty.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaProperty.java
@@ -16,11 +16,23 @@
 package org.springframework.data.mongodb.core.schema;
 
 import java.util.Collection;
+import java.util.List;
 
+import org.jspecify.annotations.Nullable;
+import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ArrayJsonSchemaProperty;
+import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.BooleanJsonSchemaProperty;
+import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.DateJsonSchemaProperty;
+import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty;
+import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.NullJsonSchemaProperty;
+import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.NumericJsonSchemaProperty;
+import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ObjectJsonSchemaProperty;
+import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.QueryableJsonSchemaProperty;
+import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.RequiredJsonSchemaProperty;
+import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.StringJsonSchemaProperty;
+import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.TimestampJsonSchemaProperty;
+import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.UntypedJsonSchemaProperty;
 import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.NumericJsonSchemaObject;
 import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject;
-import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.*;
-import org.springframework.lang.Nullable;
 
 /**
  * A {@literal property} or {@literal patternProperty} within a {@link JsonSchemaObject} of {@code type : 'object'}.
@@ -69,6 +81,18 @@ static EncryptedJsonSchemaProperty encrypted(JsonSchemaProperty property) {
 		return EncryptedJsonSchemaProperty.encrypted(property);
 	}
 
+	/**
+	 * Turns the given target property into a {@link QueryableJsonSchemaProperty queryable} one, eg. for {@literal range}
+	 * encrypted properties.
+	 * 
+	 * @param property the queryable property. Must not be {@literal null}.
+	 * @param queries predefined query characteristics.
+	 * @since 4.5
+	 */
+	static QueryableJsonSchemaProperty queryable(JsonSchemaProperty property, List<QueryCharacteristic> queries) {
+		return new QueryableJsonSchemaProperty(property, new QueryCharacteristics(queries));
+	}
+
 	/**
 	 * Creates a new {@link StringJsonSchemaProperty} with given {@literal identifier} of {@code type : 'string'}.
 	 *
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MergedJsonSchema.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MergedJsonSchema.java
index e0f3e26100..a6fc3ab8bd 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MergedJsonSchema.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MergedJsonSchema.java
@@ -19,7 +19,9 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.function.BiFunction;
+import java.util.stream.Collectors;
 
 import org.bson.Document;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MongoJsonSchema.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MongoJsonSchema.java
index f64218cc56..87c46d63dc 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MongoJsonSchema.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MongoJsonSchema.java
@@ -23,8 +23,9 @@
 import java.util.Set;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 
 /**
@@ -212,7 +213,7 @@ interface Path {
 			/**
 			 * @return the name of the currently processed element
 			 */
-			String currentElement();
+			@Nullable String currentElement();
 
 			/**
 			 * @return the path leading to the currently processed element in dot {@literal '.'} notation.
@@ -285,11 +286,11 @@ static Resolution ofValue(Path path, Object value) {
 			 * @param value the value to apply.
 			 * @return
 			 */
-			static Resolution ofValue(String key, Object value) {
+			static Resolution ofValue(@Nullable String key, Object value) {
 
 				return new Resolution() {
 					@Override
-					public String getKey() {
+					public @Nullable String getKey() {
 						return key;
 					}
 
@@ -311,8 +312,7 @@ class MongoJsonSchemaBuilder {
 
 		private ObjectJsonSchemaObject root;
 
-		@Nullable //
-		private Document encryptionMetadata;
+		private @Nullable Document encryptionMetadata;
 
 		MongoJsonSchemaBuilder() {
 			root = new ObjectJsonSchemaObject();
@@ -323,6 +323,7 @@ class MongoJsonSchemaBuilder {
 		 * @return {@code this} {@link MongoJsonSchemaBuilder}.
 		 * @see ObjectJsonSchemaObject#minProperties(int)
 		 */
+		@Contract("_ -> this")
 		public MongoJsonSchemaBuilder minProperties(int count) {
 
 			root = root.minProperties(count);
@@ -334,6 +335,7 @@ public MongoJsonSchemaBuilder minProperties(int count) {
 		 * @return {@code this} {@link MongoJsonSchemaBuilder}.
 		 * @see ObjectJsonSchemaObject#maxProperties(int)
 		 */
+		@Contract("_ -> this")
 		public MongoJsonSchemaBuilder maxProperties(int count) {
 
 			root = root.maxProperties(count);
@@ -345,6 +347,7 @@ public MongoJsonSchemaBuilder maxProperties(int count) {
 		 * @return {@code this} {@link MongoJsonSchemaBuilder}.
 		 * @see ObjectJsonSchemaObject#required(String...)
 		 */
+		@Contract("_ -> this")
 		public MongoJsonSchemaBuilder required(String... properties) {
 
 			root = root.required(properties);
@@ -356,6 +359,7 @@ public MongoJsonSchemaBuilder required(String... properties) {
 		 * @return {@code this} {@link MongoJsonSchemaBuilder}.
 		 * @see ObjectJsonSchemaObject#additionalProperties(boolean)
 		 */
+		@Contract("_ -> this")
 		public MongoJsonSchemaBuilder additionalProperties(boolean additionalPropertiesAllowed) {
 
 			root = root.additionalProperties(additionalPropertiesAllowed);
@@ -367,6 +371,7 @@ public MongoJsonSchemaBuilder additionalProperties(boolean additionalPropertiesA
 		 * @return {@code this} {@link MongoJsonSchemaBuilder}.
 		 * @see ObjectJsonSchemaObject#additionalProperties(ObjectJsonSchemaObject)
 		 */
+		@Contract("_ -> this")
 		public MongoJsonSchemaBuilder additionalProperties(ObjectJsonSchemaObject schema) {
 
 			root = root.additionalProperties(schema);
@@ -378,6 +383,7 @@ public MongoJsonSchemaBuilder additionalProperties(ObjectJsonSchemaObject schema
 		 * @return {@code this} {@link MongoJsonSchemaBuilder}.
 		 * @see ObjectJsonSchemaObject#properties(JsonSchemaProperty...)
 		 */
+		@Contract("_ -> this")
 		public MongoJsonSchemaBuilder properties(JsonSchemaProperty... properties) {
 
 			root = root.properties(properties);
@@ -389,6 +395,7 @@ public MongoJsonSchemaBuilder properties(JsonSchemaProperty... properties) {
 		 * @return {@code this} {@link MongoJsonSchemaBuilder}.
 		 * @see ObjectJsonSchemaObject#patternProperties(JsonSchemaProperty...)
 		 */
+		@Contract("_ -> this")
 		public MongoJsonSchemaBuilder patternProperties(JsonSchemaProperty... properties) {
 
 			root = root.patternProperties(properties);
@@ -400,6 +407,7 @@ public MongoJsonSchemaBuilder patternProperties(JsonSchemaProperty... properties
 		 * @return {@code this} {@link MongoJsonSchemaBuilder}.
 		 * @see ObjectJsonSchemaObject#property(JsonSchemaProperty)
 		 */
+		@Contract("_ -> this")
 		public MongoJsonSchemaBuilder property(JsonSchemaProperty property) {
 
 			root = root.property(property);
@@ -411,6 +419,7 @@ public MongoJsonSchemaBuilder property(JsonSchemaProperty property) {
 		 * @return {@code this} {@link MongoJsonSchemaBuilder}.
 		 * @see ObjectJsonSchemaObject#possibleValues(Collection)
 		 */
+		@Contract("_ -> this")
 		public MongoJsonSchemaBuilder possibleValues(Set<Object> possibleValues) {
 
 			root = root.possibleValues(possibleValues);
@@ -422,6 +431,7 @@ public MongoJsonSchemaBuilder possibleValues(Set<Object> possibleValues) {
 		 * @return {@code this} {@link MongoJsonSchemaBuilder}.
 		 * @see UntypedJsonSchemaObject#allOf(Collection)
 		 */
+		@Contract("_ -> this")
 		public MongoJsonSchemaBuilder allOf(Set<JsonSchemaObject> allOf) {
 
 			root = root.allOf(allOf);
@@ -433,6 +443,7 @@ public MongoJsonSchemaBuilder allOf(Set<JsonSchemaObject> allOf) {
 		 * @return {@code this} {@link MongoJsonSchemaBuilder}.
 		 * @see UntypedJsonSchemaObject#anyOf(Collection)
 		 */
+		@Contract("_ -> this")
 		public MongoJsonSchemaBuilder anyOf(Set<JsonSchemaObject> anyOf) {
 
 			root = root.anyOf(anyOf);
@@ -444,6 +455,7 @@ public MongoJsonSchemaBuilder anyOf(Set<JsonSchemaObject> anyOf) {
 		 * @return {@code this} {@link MongoJsonSchemaBuilder}.
 		 * @see UntypedJsonSchemaObject#oneOf(Collection)
 		 */
+		@Contract("_ -> this")
 		public MongoJsonSchemaBuilder oneOf(Set<JsonSchemaObject> oneOf) {
 
 			root = root.oneOf(oneOf);
@@ -455,6 +467,7 @@ public MongoJsonSchemaBuilder oneOf(Set<JsonSchemaObject> oneOf) {
 		 * @return {@code this} {@link MongoJsonSchemaBuilder}.
 		 * @see UntypedJsonSchemaObject#notMatch(JsonSchemaObject)
 		 */
+		@Contract("_ -> this")
 		public MongoJsonSchemaBuilder notMatch(JsonSchemaObject notMatch) {
 
 			root = root.notMatch(notMatch);
@@ -466,6 +479,7 @@ public MongoJsonSchemaBuilder notMatch(JsonSchemaObject notMatch) {
 		 * @return {@code this} {@link MongoJsonSchemaBuilder}.
 		 * @see UntypedJsonSchemaObject#description(String)
 		 */
+		@Contract("_ -> this")
 		public MongoJsonSchemaBuilder description(String description) {
 
 			root = root.description(description);
@@ -487,6 +501,7 @@ public void encryptionMetadata(@Nullable Document encryptionMetadata) {
 		 *
 		 * @return new instance of {@link MongoJsonSchema}.
 		 */
+		@Contract("-> new")
 		public MongoJsonSchema build() {
 			return new DefaultMongoJsonSchema(root, encryptionMetadata);
 		}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristic.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristic.java
new file mode 100644
index 0000000000..8604ba9d6c
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristic.java
@@ -0,0 +1,40 @@
+/*
+ * 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.mongodb.core.schema;
+
+import org.bson.Document;
+
+/**
+ * Defines the specific character of a query that can be executed. Mainly used to define the characteristic of queryable
+ * encrypted fields.
+ * 
+ * @author Christoph Strobl
+ * @since 4.5
+ */
+public interface QueryCharacteristic {
+
+	/**
+	 * @return the query type, eg. {@literal range}.
+	 */
+	String queryType();
+
+	/**
+	 * @return the raw {@link Document} representation of the instance.
+	 */
+	default Document toDocument() {
+		return new Document("queryType", queryType());
+	}
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristics.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristics.java
new file mode 100644
index 0000000000..9283bf4afa
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristics.java
@@ -0,0 +1,263 @@
+/*
+ * 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.mongodb.core.schema;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.bson.BsonNull;
+import org.bson.Document;
+import org.jspecify.annotations.Nullable;
+import org.springframework.data.domain.Range;
+import org.springframework.data.domain.Range.Bound;
+
+/**
+ * Encapsulation of individual {@link QueryCharacteristic query characteristics} used to define queries that can be
+ * executed when using queryable encryption.
+ *
+ * @author Christoph Strobl
+ * @since 4.5
+ */
+public class QueryCharacteristics implements Iterable<QueryCharacteristic> {
+
+	/**
+	 * instance indicating none
+	 */
+	private static final QueryCharacteristics NONE = new QueryCharacteristics(Collections.emptyList());
+
+	private final List<QueryCharacteristic> characteristics;
+
+	QueryCharacteristics(List<QueryCharacteristic> characteristics) {
+		this.characteristics = characteristics;
+	}
+
+	/**
+	 * @return marker instance indicating no characteristics have been defined.
+	 */
+	public static QueryCharacteristics none() {
+		return NONE;
+	}
+
+	/**
+	 * Create new {@link QueryCharacteristics} from given list of {@link QueryCharacteristic characteristics}.
+	 *
+	 * @param characteristics must not be {@literal null}.
+	 * @return new instance of {@link QueryCharacteristics}.
+	 */
+	public static QueryCharacteristics of(List<QueryCharacteristic> characteristics) {
+		return new QueryCharacteristics(List.copyOf(characteristics));
+	}
+
+	/**
+	 * Create new {@link QueryCharacteristics} from given {@link QueryCharacteristic characteristics}.
+	 *
+	 * @param characteristics must not be {@literal null}.
+	 * @return new instance of {@link QueryCharacteristics}.
+	 */
+	public static QueryCharacteristics of(QueryCharacteristic... characteristics) {
+		return new QueryCharacteristics(Arrays.asList(characteristics));
+	}
+
+	/**
+	 * @return the list of {@link QueryCharacteristic characteristics}.
+	 */
+	public List<QueryCharacteristic> getCharacteristics() {
+		return characteristics;
+	}
+
+	@Override
+	public Iterator<QueryCharacteristic> iterator() {
+		return this.characteristics.iterator();
+	}
+
+	/**
+	 * Create a new {@link RangeQuery range query characteristic} used to define range queries against an encrypted field.
+	 *
+	 * @param <T> targeted field type
+	 * @return new instance of {@link RangeQuery}.
+	 */
+	public static <T> RangeQuery<T> range() {
+		return new RangeQuery<>();
+	}
+
+	/**
+	 * Create a new {@link EqualityQuery equality query characteristic} used to define equality queries against an
+	 * encrypted field.
+	 *
+	 * @param <T> targeted field type
+	 * @return new instance of {@link EqualityQuery}.
+	 */
+	public static <T> EqualityQuery<T> equality() {
+		return new EqualityQuery<>(null);
+	}
+
+	/**
+	 * {@link QueryCharacteristic} for equality comparison.
+	 *
+	 * @param <T>
+	 * @since 4.5
+	 */
+	public static class EqualityQuery<T> implements QueryCharacteristic {
+
+		private final @Nullable Long contention;
+
+		/**
+		 * Create new instance of {@link EqualityQuery}.
+		 *
+		 * @param contention can be {@literal null}.
+		 */
+		public EqualityQuery(@Nullable Long contention) {
+			this.contention = contention;
+		}
+
+		/**
+		 * @param contention concurrent counter partition factor.
+		 * @return new instance of {@link EqualityQuery}.
+		 */
+		public EqualityQuery<T> contention(long contention) {
+			return new EqualityQuery<>(contention);
+		}
+
+		@Override
+		public String queryType() {
+			return "equality";
+		}
+
+		@Override
+		public Document toDocument() {
+			return QueryCharacteristic.super.toDocument().append("contention", contention);
+		}
+	}
+
+	/**
+	 * {@link QueryCharacteristic} for range comparison.
+	 *
+	 * @param <T>
+	 * @since 4.5
+	 */
+	public static class RangeQuery<T> implements QueryCharacteristic {
+
+		private final @Nullable Range<T> valueRange;
+		private final @Nullable Integer trimFactor;
+		private final @Nullable Long sparsity;
+		private final @Nullable Long precision;
+		private final @Nullable Long contention;
+
+		private RangeQuery() {
+			this(Range.unbounded(), null, null, null, null);
+		}
+
+		/**
+		 * Create new instance of {@link RangeQuery}.
+		 *
+		 * @param valueRange
+		 * @param trimFactor
+		 * @param sparsity
+		 * @param contention
+		 */
+		public RangeQuery(@Nullable Range<T> valueRange, @Nullable Integer trimFactor, @Nullable Long sparsity,
+				@Nullable Long precision, @Nullable Long contention) {
+			this.valueRange = valueRange;
+			this.trimFactor = trimFactor;
+			this.sparsity = sparsity;
+			this.precision = precision;
+			this.contention = contention;
+		}
+
+		/**
+		 * @param lower the lower value range boundary for the queryable field.
+		 * @return new instance of {@link RangeQuery}.
+		 */
+		public RangeQuery<T> min(T lower) {
+
+			Range<T> range = Range.of(Bound.inclusive(lower),
+					valueRange != null ? valueRange.getUpperBound() : Bound.unbounded());
+			return new RangeQuery<>(range, trimFactor, sparsity, precision, contention);
+		}
+
+		/**
+		 * @param upper the upper value range boundary for the queryable field.
+		 * @return new instance of {@link RangeQuery}.
+		 */
+		public RangeQuery<T> max(T upper) {
+
+			Range<T> range = Range.of(valueRange != null ? valueRange.getLowerBound() : Bound.unbounded(),
+					Bound.inclusive(upper));
+			return new RangeQuery<>(range, trimFactor, sparsity, precision, contention);
+		}
+
+		/**
+		 * @param trimFactor value to control the throughput of concurrent inserts and updates.
+		 * @return new instance of {@link RangeQuery}.
+		 */
+		public RangeQuery<T> trimFactor(int trimFactor) {
+			return new RangeQuery<>(valueRange, trimFactor, sparsity, precision, contention);
+		}
+
+		/**
+		 * @param sparsity value to control the value density within the index.
+		 * @return new instance of {@link RangeQuery}.
+		 */
+		public RangeQuery<T> sparsity(long sparsity) {
+			return new RangeQuery<>(valueRange, trimFactor, sparsity, precision, contention);
+		}
+
+		/**
+		 * @param contention concurrent counter partition factor.
+		 * @return new instance of {@link RangeQuery}.
+		 */
+		public RangeQuery<T> contention(long contention) {
+			return new RangeQuery<>(valueRange, trimFactor, sparsity, precision, contention);
+		}
+
+		/**
+		 * @param precision digits considered comparing floating point numbers.
+		 * @return new instance of {@link RangeQuery}.
+		 */
+		public RangeQuery<T> precision(long precision) {
+			return new RangeQuery<>(valueRange, trimFactor, sparsity, precision, contention);
+		}
+
+		@Override
+		public String queryType() {
+			return "range";
+		}
+
+		@Override
+		@SuppressWarnings("unchecked")
+		public Document toDocument() {
+
+			Document target = QueryCharacteristic.super.toDocument();
+			if (contention != null) {
+				target.append("contention", contention);
+			}
+			if (trimFactor != null) {
+				target.append("trimFactor", trimFactor);
+			}
+			if (valueRange != null) {
+				target.append("min", valueRange.getLowerBound().getValue().orElse((T) BsonNull.VALUE)).append("max",
+						valueRange.getUpperBound().getValue().orElse((T) BsonNull.VALUE));
+			}
+			if (sparsity != null) {
+				target.append("sparsity", sparsity);
+			}
+
+			return target;
+		}
+	}
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypeUnifyingMergeFunction.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypeUnifyingMergeFunction.java
index 95f116619f..87bdd8c618 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypeUnifyingMergeFunction.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypeUnifyingMergeFunction.java
@@ -22,10 +22,10 @@
 import java.util.function.BiFunction;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.schema.MongoJsonSchema.ConflictResolutionFunction;
 import org.springframework.data.mongodb.core.schema.MongoJsonSchema.ConflictResolutionFunction.Path;
 import org.springframework.data.mongodb.core.schema.MongoJsonSchema.ConflictResolutionFunction.Resolution;
-import org.springframework.lang.Nullable;
 import org.springframework.util.CollectionUtils;
 import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
@@ -119,8 +119,7 @@ private static String getTypeKeyToUse(String key, Document source) {
 		return key;
 	}
 
-	@Nullable
-	private static Object getUnifiedExistingType(String key, Document source) {
+	private static @Nullable Object getUnifiedExistingType(String key, Document source) {
 		return source.get(getTypeKeyToUse(key, source));
 	}
 
@@ -155,7 +154,7 @@ public SimplePath append(String next) {
 		}
 
 		@Override
-		public String currentElement() {
+		public @Nullable String currentElement() {
 			return CollectionUtils.lastElement(path);
 		}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypedJsonSchemaObject.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypedJsonSchemaObject.java
index abf8b0b8a2..7b299fd4d2 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypedJsonSchemaObject.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypedJsonSchemaObject.java
@@ -27,9 +27,10 @@
 import java.util.stream.Collectors;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.domain.Range;
 import org.springframework.data.domain.Range.Bound;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.CollectionUtils;
 import org.springframework.util.ObjectUtils;
@@ -100,6 +101,7 @@ public Set<Type> getTypes() {
 	 * @return new instance of {@link TypedJsonSchemaObject}.
 	 */
 	@Override
+	@Contract("_ -> new")
 	public TypedJsonSchemaObject description(String description) {
 		return new TypedJsonSchemaObject(types, description, generateDescription, restrictions);
 	}
@@ -110,6 +112,7 @@ public TypedJsonSchemaObject description(String description) {
 	 * @return new instance of {@link TypedJsonSchemaObject}.
 	 */
 	@Override
+	@Contract("-> new")
 	public TypedJsonSchemaObject generatedDescription() {
 		return new TypedJsonSchemaObject(types, description, true, restrictions);
 	}
@@ -121,6 +124,7 @@ public TypedJsonSchemaObject generatedDescription() {
 	 * @return new instance of {@link TypedJsonSchemaObject}.
 	 */
 	@Override
+	@Contract("_ -> new")
 	public TypedJsonSchemaObject possibleValues(Collection<? extends Object> possibleValues) {
 		return new TypedJsonSchemaObject(types, description, generateDescription,
 				restrictions.possibleValues(possibleValues));
@@ -133,6 +137,7 @@ public TypedJsonSchemaObject possibleValues(Collection<? extends Object> possibl
 	 * @return new instance of {@link TypedJsonSchemaObject}.
 	 */
 	@Override
+	@Contract("_ -> new")
 	public TypedJsonSchemaObject allOf(Collection<JsonSchemaObject> allOf) {
 		return new TypedJsonSchemaObject(types, description, generateDescription, restrictions.allOf(allOf));
 	}
@@ -144,6 +149,7 @@ public TypedJsonSchemaObject allOf(Collection<JsonSchemaObject> allOf) {
 	 * @return new instance of {@link TypedJsonSchemaObject}.
 	 */
 	@Override
+	@Contract("_ -> new")
 	public TypedJsonSchemaObject anyOf(Collection<JsonSchemaObject> anyOf) {
 		return new TypedJsonSchemaObject(types, description, generateDescription, restrictions.anyOf(anyOf));
 	}
@@ -155,6 +161,7 @@ public TypedJsonSchemaObject anyOf(Collection<JsonSchemaObject> anyOf) {
 	 * @return new instance of {@link TypedJsonSchemaObject}.
 	 */
 	@Override
+	@Contract("_ -> new")
 	public TypedJsonSchemaObject oneOf(Collection<JsonSchemaObject> oneOf) {
 		return new TypedJsonSchemaObject(types, description, generateDescription, restrictions.oneOf(oneOf));
 	}
@@ -166,6 +173,7 @@ public TypedJsonSchemaObject oneOf(Collection<JsonSchemaObject> oneOf) {
 	 * @return new instance of {@link TypedJsonSchemaObject}.
 	 */
 	@Override
+	@Contract("_ -> new")
 	public TypedJsonSchemaObject notMatch(JsonSchemaObject notMatch) {
 		return new TypedJsonSchemaObject(types, description, generateDescription, restrictions.notMatch(notMatch));
 	}
@@ -210,8 +218,7 @@ private Optional<String> getOrCreateDescription() {
 	 *
 	 * @return can be {@literal null}.
 	 */
-	@Nullable
-	protected String generateDescription() {
+	protected @Nullable String generateDescription() {
 		return null;
 	}
 
@@ -264,6 +271,7 @@ public ObjectJsonSchemaObject propertiesCount(Range<Integer> range) {
 		 * @param count the allowed minimal number of properties.
 		 * @return new instance of {@link ObjectJsonSchemaObject}.
 		 */
+		@Contract("_ -> new")
 		public ObjectJsonSchemaObject minProperties(int count) {
 
 			Bound<Integer> upper = this.propertiesCount != null ? this.propertiesCount.getUpperBound() : Bound.unbounded();
@@ -276,6 +284,7 @@ public ObjectJsonSchemaObject minProperties(int count) {
 		 * @param count the allowed maximum number of properties.
 		 * @return new instance of {@link ObjectJsonSchemaObject}.
 		 */
+		@Contract("_ -> new")
 		public ObjectJsonSchemaObject maxProperties(int count) {
 
 			Bound<Integer> lower = this.propertiesCount != null ? this.propertiesCount.getLowerBound() : Bound.unbounded();
@@ -288,6 +297,7 @@ public ObjectJsonSchemaObject maxProperties(int count) {
 		 * @param properties the names of required properties.
 		 * @return new instance of {@link ObjectJsonSchemaObject}.
 		 */
+		@Contract("_ -> new")
 		public ObjectJsonSchemaObject required(String... properties) {
 
 			ObjectJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions);
@@ -305,6 +315,7 @@ public ObjectJsonSchemaObject required(String... properties) {
 		 * @param additionalPropertiesAllowed
 		 * @return new instance of {@link ObjectJsonSchemaObject}.
 		 */
+		@Contract("_ -> new")
 		public ObjectJsonSchemaObject additionalProperties(boolean additionalPropertiesAllowed) {
 
 			ObjectJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions);
@@ -319,6 +330,7 @@ public ObjectJsonSchemaObject additionalProperties(boolean additionalPropertiesA
 		 * @param schema must not be {@literal null}.
 		 * @return new instance of {@link ObjectJsonSchemaObject}.
 		 */
+		@Contract("_ -> new")
 		public ObjectJsonSchemaObject additionalProperties(ObjectJsonSchemaObject schema) {
 
 			ObjectJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions);
@@ -332,6 +344,7 @@ public ObjectJsonSchemaObject additionalProperties(ObjectJsonSchemaObject schema
 		 * @param properties must not be {@literal null}.
 		 * @return new instance of {@link ObjectJsonSchemaObject}.
 		 */
+		@Contract("_ -> new")
 		public ObjectJsonSchemaObject properties(JsonSchemaProperty... properties) {
 
 			ObjectJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions);
@@ -349,6 +362,7 @@ public ObjectJsonSchemaObject properties(JsonSchemaProperty... properties) {
 		 * @param regularExpressions must not be {@literal null}.
 		 * @return new instance of {@link ObjectJsonSchemaObject}.
 		 */
+		@Contract("_ -> new")
 		public ObjectJsonSchemaObject patternProperties(JsonSchemaProperty... regularExpressions) {
 
 			ObjectJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions);
@@ -365,41 +379,49 @@ public ObjectJsonSchemaObject patternProperties(JsonSchemaProperty... regularExp
 		 * @param property must not be {@literal null}.
 		 * @return new instance of {@link ObjectJsonSchemaObject}.
 		 */
+		@Contract("_ -> new")
 		public ObjectJsonSchemaObject property(JsonSchemaProperty property) {
 			return properties(property);
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public ObjectJsonSchemaObject possibleValues(Collection<? extends Object> possibleValues) {
 			return newInstance(description, generateDescription, restrictions.possibleValues(possibleValues));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public ObjectJsonSchemaObject allOf(Collection<JsonSchemaObject> allOf) {
 			return newInstance(description, generateDescription, restrictions.allOf(allOf));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public ObjectJsonSchemaObject anyOf(Collection<JsonSchemaObject> anyOf) {
 			return newInstance(description, generateDescription, restrictions.anyOf(anyOf));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public ObjectJsonSchemaObject oneOf(Collection<JsonSchemaObject> oneOf) {
 			return newInstance(description, generateDescription, restrictions.oneOf(oneOf));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public ObjectJsonSchemaObject notMatch(JsonSchemaObject notMatch) {
 			return newInstance(description, generateDescription, restrictions.notMatch(notMatch));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public ObjectJsonSchemaObject description(String description) {
 			return newInstance(description, generateDescription, restrictions);
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public ObjectJsonSchemaObject generatedDescription() {
 			return newInstance(description, true, restrictions);
 		}
@@ -545,6 +567,7 @@ private NumericJsonSchemaObject(Set<Type> types, @Nullable String description, b
 		 * @param value must not be {@literal null}.
 		 * @return must not be {@literal null}.
 		 */
+		@Contract("_ -> new")
 		public NumericJsonSchemaObject multipleOf(Number value) {
 
 			Assert.notNull(value, "Value must not be null");
@@ -561,6 +584,7 @@ public NumericJsonSchemaObject multipleOf(Number value) {
 		 * @param range must not be {@literal null}.
 		 * @return new instance of {@link NumericJsonSchemaObject}.
 		 */
+		@Contract("_ -> new")
 		public NumericJsonSchemaObject within(Range<? extends Number> range) {
 
 			Assert.notNull(range, "Range must not be null");
@@ -578,6 +602,7 @@ public NumericJsonSchemaObject within(Range<? extends Number> range) {
 		 * @return new instance of {@link NumericJsonSchemaObject}.
 		 */
 		@SuppressWarnings("unchecked")
+		@Contract("_ -> new")
 		public NumericJsonSchemaObject gt(Number min) {
 
 			Assert.notNull(min, "Min must not be null");
@@ -593,6 +618,7 @@ public NumericJsonSchemaObject gt(Number min) {
 		 * @return new instance of {@link NumericJsonSchemaObject}.
 		 */
 		@SuppressWarnings("unchecked")
+		@Contract("_ -> new")
 		public NumericJsonSchemaObject gte(Number min) {
 
 			Assert.notNull(min, "Min must not be null");
@@ -608,6 +634,7 @@ public NumericJsonSchemaObject gte(Number min) {
 		 * @return new instance of {@link NumericJsonSchemaObject}.
 		 */
 		@SuppressWarnings("unchecked")
+		@Contract("_ -> new")
 		public NumericJsonSchemaObject lt(Number max) {
 
 			Assert.notNull(max, "Max must not be null");
@@ -623,6 +650,7 @@ public NumericJsonSchemaObject lt(Number max) {
 		 * @return new instance of {@link NumericJsonSchemaObject}.
 		 */
 		@SuppressWarnings("unchecked")
+		@Contract("_ -> new")
 		public NumericJsonSchemaObject lte(Number max) {
 
 			Assert.notNull(max, "Max must not be null");
@@ -632,36 +660,43 @@ public NumericJsonSchemaObject lte(Number max) {
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public NumericJsonSchemaObject possibleValues(Collection<? extends Object> possibleValues) {
 			return newInstance(description, generateDescription, restrictions.possibleValues(possibleValues));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public NumericJsonSchemaObject allOf(Collection<JsonSchemaObject> allOf) {
 			return newInstance(description, generateDescription, restrictions.allOf(allOf));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public NumericJsonSchemaObject anyOf(Collection<JsonSchemaObject> anyOf) {
 			return newInstance(description, generateDescription, restrictions.anyOf(anyOf));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public NumericJsonSchemaObject oneOf(Collection<JsonSchemaObject> oneOf) {
 			return newInstance(description, generateDescription, restrictions.oneOf(oneOf));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public NumericJsonSchemaObject notMatch(JsonSchemaObject notMatch) {
 			return newInstance(description, generateDescription, restrictions.notMatch(notMatch));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public NumericJsonSchemaObject description(String description) {
 			return newInstance(description, generateDescription, restrictions);
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public NumericJsonSchemaObject generatedDescription() {
 			return newInstance(description, true, restrictions);
 		}
@@ -785,6 +820,7 @@ private StringJsonSchemaObject(@Nullable String description, boolean generateDes
 		 * @param range must not be {@literal null}.
 		 * @return new instance of {@link StringJsonSchemaObject}.
 		 */
+		@Contract("_ -> new")
 		public StringJsonSchemaObject length(Range<Integer> range) {
 
 			Assert.notNull(range, "Range must not be null");
@@ -801,6 +837,7 @@ public StringJsonSchemaObject length(Range<Integer> range) {
 		 * @param length
 		 * @return new instance of {@link StringJsonSchemaObject}.
 		 */
+		@Contract("_ -> new")
 		public StringJsonSchemaObject minLength(int length) {
 
 			Bound<Integer> upper = this.length != null ? this.length.getUpperBound() : Bound.unbounded();
@@ -813,6 +850,7 @@ public StringJsonSchemaObject minLength(int length) {
 		 * @param length
 		 * @return new instance of {@link StringJsonSchemaObject}.
 		 */
+		@Contract("_ -> new")
 		public StringJsonSchemaObject maxLength(int length) {
 
 			Bound<Integer> lower = this.length != null ? this.length.getLowerBound() : Bound.unbounded();
@@ -825,6 +863,7 @@ public StringJsonSchemaObject maxLength(int length) {
 		 * @param pattern must not be {@literal null}.
 		 * @return new instance of {@link StringJsonSchemaObject}.
 		 */
+		@Contract("_ -> new")
 		public StringJsonSchemaObject matching(String pattern) {
 
 			Assert.notNull(pattern, "Pattern must not be null");
@@ -836,36 +875,43 @@ public StringJsonSchemaObject matching(String pattern) {
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public StringJsonSchemaObject possibleValues(Collection<? extends Object> possibleValues) {
 			return newInstance(description, generateDescription, restrictions.possibleValues(possibleValues));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public StringJsonSchemaObject allOf(Collection<JsonSchemaObject> allOf) {
 			return newInstance(description, generateDescription, restrictions.allOf(allOf));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public StringJsonSchemaObject anyOf(Collection<JsonSchemaObject> anyOf) {
 			return newInstance(description, generateDescription, restrictions.anyOf(anyOf));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public StringJsonSchemaObject oneOf(Collection<JsonSchemaObject> oneOf) {
 			return newInstance(description, generateDescription, restrictions.oneOf(oneOf));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public StringJsonSchemaObject notMatch(JsonSchemaObject notMatch) {
 			return newInstance(description, generateDescription, restrictions.notMatch(notMatch));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public StringJsonSchemaObject description(String description) {
 			return newInstance(description, generateDescription, restrictions);
 		}
 
 		@Override
+		@Contract("-> new")
 		public StringJsonSchemaObject generatedDescription() {
 			return newInstance(description, true, restrictions);
 		}
@@ -946,6 +992,7 @@ private ArrayJsonSchemaObject(@Nullable String description, boolean generateDesc
 		 * @param uniqueItems
 		 * @return new instance of {@link ArrayJsonSchemaObject}.
 		 */
+		@Contract("_ -> new")
 		public ArrayJsonSchemaObject uniqueItems(boolean uniqueItems) {
 
 			ArrayJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions);
@@ -961,6 +1008,7 @@ public ArrayJsonSchemaObject uniqueItems(boolean uniqueItems) {
 		 * @param range must not be {@literal null}. Consider {@link Range#unbounded()} instead.
 		 * @return new instance of {@link ArrayJsonSchemaObject}.
 		 */
+		@Contract("_ -> new")
 		public ArrayJsonSchemaObject range(Range<Integer> range) {
 
 			ArrayJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions);
@@ -975,6 +1023,7 @@ public ArrayJsonSchemaObject range(Range<Integer> range) {
 		 * @param count the allowed minimal number of array items.
 		 * @return new instance of {@link ArrayJsonSchemaObject}.
 		 */
+		@Contract("_ -> new")
 		public ArrayJsonSchemaObject minItems(int count) {
 
 			Bound<Integer> upper = this.range != null ? this.range.getUpperBound() : Bound.unbounded();
@@ -987,6 +1036,7 @@ public ArrayJsonSchemaObject minItems(int count) {
 		 * @param count the allowed maximal number of array items.
 		 * @return new instance of {@link ArrayJsonSchemaObject}.
 		 */
+		@Contract("_ -> new")
 		public ArrayJsonSchemaObject maxItems(int count) {
 
 			Bound<Integer> lower = this.range != null ? this.range.getLowerBound() : Bound.unbounded();
@@ -999,6 +1049,7 @@ public ArrayJsonSchemaObject maxItems(int count) {
 		 * @param items the allowed items in the array.
 		 * @return new instance of {@link ArrayJsonSchemaObject}.
 		 */
+		@Contract("_ -> new")
 		public ArrayJsonSchemaObject items(Collection<JsonSchemaObject> items) {
 
 			ArrayJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions);
@@ -1013,6 +1064,7 @@ public ArrayJsonSchemaObject items(Collection<JsonSchemaObject> items) {
 		 * @param additionalItemsAllowed {@literal true} to allow additional items in the array, {@literal false} otherwise.
 		 * @return new instance of {@link ArrayJsonSchemaObject}.
 		 */
+		@Contract("_ -> new")
 		public ArrayJsonSchemaObject additionalItems(boolean additionalItemsAllowed) {
 
 			ArrayJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions);
@@ -1022,36 +1074,43 @@ public ArrayJsonSchemaObject additionalItems(boolean additionalItemsAllowed) {
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public ArrayJsonSchemaObject possibleValues(Collection<? extends Object> possibleValues) {
 			return newInstance(description, generateDescription, restrictions.possibleValues(possibleValues));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public ArrayJsonSchemaObject allOf(Collection<JsonSchemaObject> allOf) {
 			return newInstance(description, generateDescription, restrictions.allOf(allOf));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public ArrayJsonSchemaObject anyOf(Collection<JsonSchemaObject> anyOf) {
 			return newInstance(description, generateDescription, restrictions.anyOf(anyOf));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public ArrayJsonSchemaObject oneOf(Collection<JsonSchemaObject> oneOf) {
 			return newInstance(description, generateDescription, restrictions.oneOf(oneOf));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public ArrayJsonSchemaObject notMatch(JsonSchemaObject notMatch) {
 			return newInstance(description, generateDescription, restrictions.notMatch(notMatch));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public ArrayJsonSchemaObject description(String description) {
 			return newInstance(description, generateDescription, restrictions);
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public ArrayJsonSchemaObject generatedDescription() {
 			return newInstance(description, true, restrictions);
 		}
@@ -1147,41 +1206,49 @@ private BooleanJsonSchemaObject(@Nullable String description, boolean generateDe
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public BooleanJsonSchemaObject possibleValues(Collection<? extends Object> possibleValues) {
 			return new BooleanJsonSchemaObject(description, generateDescription, restrictions.possibleValues(possibleValues));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public BooleanJsonSchemaObject allOf(Collection<JsonSchemaObject> allOf) {
 			return new BooleanJsonSchemaObject(description, generateDescription, restrictions.allOf(allOf));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public BooleanJsonSchemaObject anyOf(Collection<JsonSchemaObject> anyOf) {
 			return new BooleanJsonSchemaObject(description, generateDescription, restrictions.anyOf(anyOf));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public BooleanJsonSchemaObject oneOf(Collection<JsonSchemaObject> oneOf) {
 			return new BooleanJsonSchemaObject(description, generateDescription, restrictions.oneOf(oneOf));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public BooleanJsonSchemaObject notMatch(JsonSchemaObject notMatch) {
 			return new BooleanJsonSchemaObject(description, generateDescription, restrictions.notMatch(notMatch));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public BooleanJsonSchemaObject description(String description) {
 			return new BooleanJsonSchemaObject(description, generateDescription, restrictions);
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public BooleanJsonSchemaObject generatedDescription() {
 			return new BooleanJsonSchemaObject(description, true, restrictions);
 		}
 
 		@Override
+		@Contract("-> new")
 		protected String generateDescription() {
 			return "Must be a boolean";
 		}
@@ -1208,36 +1275,43 @@ private NullJsonSchemaObject(@Nullable String description, boolean generateDescr
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public NullJsonSchemaObject possibleValues(Collection<? extends Object> possibleValues) {
 			return new NullJsonSchemaObject(description, generateDescription, restrictions.possibleValues(possibleValues));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public NullJsonSchemaObject allOf(Collection<JsonSchemaObject> allOf) {
 			return new NullJsonSchemaObject(description, generateDescription, restrictions.allOf(allOf));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public NullJsonSchemaObject anyOf(Collection<JsonSchemaObject> anyOf) {
 			return new NullJsonSchemaObject(description, generateDescription, restrictions.anyOf(anyOf));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public NullJsonSchemaObject oneOf(Collection<JsonSchemaObject> oneOf) {
 			return new NullJsonSchemaObject(description, generateDescription, restrictions.oneOf(oneOf));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public NullJsonSchemaObject notMatch(JsonSchemaObject notMatch) {
 			return new NullJsonSchemaObject(description, generateDescription, restrictions.notMatch(notMatch));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public NullJsonSchemaObject description(String description) {
 			return new NullJsonSchemaObject(description, generateDescription, restrictions);
 		}
 
 		@Override
+		@Contract("-> new")
 		public NullJsonSchemaObject generatedDescription() {
 			return new NullJsonSchemaObject(description, true, restrictions);
 		}
@@ -1268,36 +1342,43 @@ private DateJsonSchemaObject(@Nullable String description, boolean generateDescr
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public DateJsonSchemaObject possibleValues(Collection<? extends Object> possibleValues) {
 			return new DateJsonSchemaObject(description, generateDescription, restrictions.possibleValues(possibleValues));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public DateJsonSchemaObject allOf(Collection<JsonSchemaObject> allOf) {
 			return new DateJsonSchemaObject(description, generateDescription, restrictions.allOf(allOf));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public DateJsonSchemaObject anyOf(Collection<JsonSchemaObject> anyOf) {
 			return new DateJsonSchemaObject(description, generateDescription, restrictions.anyOf(anyOf));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public DateJsonSchemaObject oneOf(Collection<JsonSchemaObject> oneOf) {
 			return new DateJsonSchemaObject(description, generateDescription, restrictions.oneOf(oneOf));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public DateJsonSchemaObject notMatch(JsonSchemaObject notMatch) {
 			return new DateJsonSchemaObject(description, generateDescription, restrictions.notMatch(notMatch));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public DateJsonSchemaObject description(String description) {
 			return new DateJsonSchemaObject(description, generateDescription, restrictions);
 		}
 
 		@Override
+		@Contract("-> new")
 		public DateJsonSchemaObject generatedDescription() {
 			return new DateJsonSchemaObject(description, true, restrictions);
 		}
@@ -1328,37 +1409,44 @@ private TimestampJsonSchemaObject(@Nullable String description, boolean generate
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public TimestampJsonSchemaObject possibleValues(Collection<? extends Object> possibleValues) {
 			return new TimestampJsonSchemaObject(description, generateDescription,
 					restrictions.possibleValues(possibleValues));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public TimestampJsonSchemaObject allOf(Collection<JsonSchemaObject> allOf) {
 			return new TimestampJsonSchemaObject(description, generateDescription, restrictions.allOf(allOf));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public TimestampJsonSchemaObject anyOf(Collection<JsonSchemaObject> anyOf) {
 			return new TimestampJsonSchemaObject(description, generateDescription, restrictions.anyOf(anyOf));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public TimestampJsonSchemaObject oneOf(Collection<JsonSchemaObject> oneOf) {
 			return new TimestampJsonSchemaObject(description, generateDescription, restrictions.oneOf(oneOf));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public TimestampJsonSchemaObject notMatch(JsonSchemaObject notMatch) {
 			return new TimestampJsonSchemaObject(description, generateDescription, restrictions.notMatch(notMatch));
 		}
 
 		@Override
+		@Contract("_ -> new")
 		public TimestampJsonSchemaObject description(String description) {
 			return new TimestampJsonSchemaObject(description, generateDescription, restrictions);
 		}
 
 		@Override
+		@Contract("-> new")
 		public TimestampJsonSchemaObject generatedDescription() {
 			return new TimestampJsonSchemaObject(description, true, restrictions);
 		}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/UntypedJsonSchemaObject.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/UntypedJsonSchemaObject.java
index 54ca29e0e3..d13f8d7985 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/UntypedJsonSchemaObject.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/UntypedJsonSchemaObject.java
@@ -23,7 +23,8 @@
 import java.util.stream.Collectors;
 
 import org.bson.Document;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.CollectionUtils;
 
@@ -69,6 +70,7 @@ public Set<Type> getTypes() {
 	 * @param description must not be {@literal null}.
 	 * @return new instance of {@link TypedJsonSchemaObject}.
 	 */
+	@Contract("_ -> new")
 	public UntypedJsonSchemaObject description(String description) {
 		return new UntypedJsonSchemaObject(restrictions, description, generateDescription);
 	}
@@ -78,6 +80,7 @@ public UntypedJsonSchemaObject description(String description) {
 	 *
 	 * @return new instance of {@link TypedJsonSchemaObject}.
 	 */
+	@Contract("-> new")
 	public UntypedJsonSchemaObject generatedDescription() {
 		return new UntypedJsonSchemaObject(restrictions, description, true);
 	}
@@ -88,6 +91,7 @@ public UntypedJsonSchemaObject generatedDescription() {
 	 * @param possibleValues must not be {@literal null}.
 	 * @return new instance of {@link TypedJsonSchemaObject}.
 	 */
+	@Contract("_ -> new")
 	public UntypedJsonSchemaObject possibleValues(Collection<? extends Object> possibleValues) {
 		return new UntypedJsonSchemaObject(restrictions.possibleValues(possibleValues), description, generateDescription);
 	}
@@ -98,6 +102,7 @@ public UntypedJsonSchemaObject possibleValues(Collection<? extends Object> possi
 	 * @param allOf must not be {@literal null}.
 	 * @return new instance of {@link TypedJsonSchemaObject}.
 	 */
+	@Contract("_ -> new")
 	public UntypedJsonSchemaObject allOf(Collection<JsonSchemaObject> allOf) {
 		return new UntypedJsonSchemaObject(restrictions.allOf(allOf), description, generateDescription);
 	}
@@ -108,6 +113,7 @@ public UntypedJsonSchemaObject allOf(Collection<JsonSchemaObject> allOf) {
 	 * @param anyOf must not be {@literal null}.
 	 * @return new instance of {@link TypedJsonSchemaObject}.
 	 */
+	@Contract("_ -> new")
 	public UntypedJsonSchemaObject anyOf(Collection<JsonSchemaObject> anyOf) {
 		return new UntypedJsonSchemaObject(restrictions.anyOf(anyOf), description, generateDescription);
 	}
@@ -118,6 +124,7 @@ public UntypedJsonSchemaObject anyOf(Collection<JsonSchemaObject> anyOf) {
 	 * @param oneOf must not be {@literal null}.
 	 * @return new instance of {@link TypedJsonSchemaObject}.
 	 */
+	@Contract("_ -> new")
 	public UntypedJsonSchemaObject oneOf(Collection<JsonSchemaObject> oneOf) {
 		return new UntypedJsonSchemaObject(restrictions.oneOf(oneOf), description, generateDescription);
 	}
@@ -128,6 +135,7 @@ public UntypedJsonSchemaObject oneOf(Collection<JsonSchemaObject> oneOf) {
 	 * @param notMatch must not be {@literal null}.
 	 * @return new instance of {@link TypedJsonSchemaObject}.
 	 */
+	@Contract("_ -> new")
 	public UntypedJsonSchemaObject notMatch(JsonSchemaObject notMatch) {
 		return new UntypedJsonSchemaObject(restrictions.notMatch(notMatch), description, generateDescription);
 	}
@@ -163,8 +171,7 @@ private Optional<String> getOrCreateDescription() {
 	 *
 	 * @return can be {@literal null}.
 	 */
-	@Nullable
-	protected String generateDescription() {
+	protected @Nullable String generateDescription() {
 		return null;
 	}
 
@@ -177,14 +184,14 @@ protected String generateDescription() {
 	 */
 	static class Restrictions {
 
-		private final Collection<? extends Object> possibleValues;
+		private final Collection<?> possibleValues;
 		private final Collection<JsonSchemaObject> allOf;
 		private final Collection<JsonSchemaObject> anyOf;
 		private final Collection<JsonSchemaObject> oneOf;
 		private final @Nullable JsonSchemaObject notMatch;
 
-		Restrictions(Collection<? extends Object> possibleValues, Collection<JsonSchemaObject> allOf,
-				Collection<JsonSchemaObject> anyOf, Collection<JsonSchemaObject> oneOf, JsonSchemaObject notMatch) {
+		Restrictions(Collection<?> possibleValues, Collection<JsonSchemaObject> allOf,
+				Collection<JsonSchemaObject> anyOf, Collection<JsonSchemaObject> oneOf, @Nullable JsonSchemaObject notMatch) {
 
 			this.possibleValues = possibleValues;
 			this.allOf = allOf;
@@ -206,7 +213,8 @@ static Restrictions empty() {
 		 * @param possibleValues must not be {@literal null}.
 		 * @return
 		 */
-		Restrictions possibleValues(Collection<? extends Object> possibleValues) {
+		@Contract("_ -> new")
+		Restrictions possibleValues(Collection<?> possibleValues) {
 
 			Assert.notNull(possibleValues, "PossibleValues must not be null");
 			return new Restrictions(possibleValues, allOf, anyOf, oneOf, notMatch);
@@ -216,6 +224,7 @@ Restrictions possibleValues(Collection<? extends Object> possibleValues) {
 		 * @param allOf must not be {@literal null}.
 		 * @return
 		 */
+		@Contract("_ -> new")
 		Restrictions allOf(Collection<JsonSchemaObject> allOf) {
 
 			Assert.notNull(allOf, "AllOf must not be null");
@@ -226,6 +235,7 @@ Restrictions allOf(Collection<JsonSchemaObject> allOf) {
 		 * @param anyOf must not be {@literal null}.
 		 * @return
 		 */
+		@Contract("_ -> new")
 		Restrictions anyOf(Collection<JsonSchemaObject> anyOf) {
 
 			Assert.notNull(anyOf, "AnyOf must not be null");
@@ -236,6 +246,7 @@ Restrictions anyOf(Collection<JsonSchemaObject> anyOf) {
 		 * @param oneOf must not be {@literal null}.
 		 * @return
 		 */
+		@Contract("_ -> new")
 		Restrictions oneOf(Collection<JsonSchemaObject> oneOf) {
 
 			Assert.notNull(oneOf, "OneOf must not be null");
@@ -246,6 +257,7 @@ Restrictions oneOf(Collection<JsonSchemaObject> oneOf) {
 		 * @param notMatch must not be {@literal null}.
 		 * @return
 		 */
+		@Contract("_ -> new")
 		Restrictions notMatch(JsonSchemaObject notMatch) {
 
 			Assert.notNull(notMatch, "NotMatch must not be null");
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/package-info.java
index 380d92af09..cdc583e038 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/package-info.java
@@ -1,6 +1,6 @@
 /**
  * MongoDB-specific JSON schema implementation classes.
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 @org.springframework.lang.NonNullFields
 package org.springframework.data.mongodb.core.schema;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/package-info.java
index 34eb8ea890..976b238fbd 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/package-info.java
@@ -3,6 +3,6 @@
  *
  * @since 1.7
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 package org.springframework.data.mongodb.core.script;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionNode.java
index b4550ee8de..a5b4a2aabf 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionNode.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionNode.java
@@ -18,13 +18,13 @@
 import java.util.Collections;
 import java.util.Iterator;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.expression.spel.ExpressionState;
 import org.springframework.expression.spel.SpelNode;
 import org.springframework.expression.spel.ast.Literal;
 import org.springframework.expression.spel.ast.MethodReference;
 import org.springframework.expression.spel.ast.Operator;
 import org.springframework.expression.spel.ast.OperatorNot;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
 /**
@@ -150,8 +150,7 @@ public boolean isLiteral() {
 	 *
 	 * @return
 	 */
-	@Nullable
-	public Object getValue() {
+	public @Nullable Object getValue() {
 		return node.getValue(state);
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformationContextSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformationContextSupport.java
index 8869f51e09..89edd4eab2 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformationContextSupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformationContextSupport.java
@@ -18,7 +18,7 @@
 import java.util.List;
 
 import org.bson.Document;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.Assert;
 
 /**
@@ -67,8 +67,7 @@ public T getCurrentNode() {
 	 *
 	 * @return
 	 */
-	@Nullable
-	public ExpressionNode getParentNode() {
+	public @Nullable ExpressionNode getParentNode() {
 		return parentNode;
 	}
 
@@ -81,8 +80,7 @@ public ExpressionNode getParentNode() {
 	 * @see #addToPreviousOrReturn(Object)
 	 * @return
 	 */
-	@Nullable
-	public Document getPreviousOperationObject() {
+	public @Nullable Document getPreviousOperationObject() {
 		return previousOperationObject;
 	}
 
@@ -110,7 +108,7 @@ public boolean parentIsSameOperation() {
 	 * @param value
 	 * @return
 	 */
-	public Document addToPreviousOperation(Object value) {
+	public Document addToPreviousOperation(@Nullable Object value) {
 
 		Assert.state(previousOperationObject != null, "No previous operation available");
 
@@ -124,11 +122,14 @@ public Document addToPreviousOperation(Object value) {
 	 * @param value
 	 * @return
 	 */
-	public Object addToPreviousOrReturn(Object value) {
+	public @Nullable Object addToPreviousOrReturn(@Nullable Object value) {
 		return hasPreviousOperation() ? addToPreviousOperation(value) : value;
 	}
 
+	@SuppressWarnings("unchecked")
 	private List<Object> extractArgumentListFrom(Document context) {
-		return (List<Object>) context.get(context.keySet().iterator().next());
+
+		Object o = context.get(context.keySet().iterator().next());
+		return o instanceof List<?> l ? (List<Object>) l : List.of();
 	}
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformer.java
index 512f753042..da5748f523 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformer.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformer.java
@@ -15,6 +15,8 @@
  */
 package org.springframework.data.mongodb.core.spel;
 
+import org.jspecify.annotations.Nullable;
+
 /**
  * SPI interface to implement components that can transform an {@link ExpressionTransformationContextSupport} into an
  * object.
@@ -29,5 +31,5 @@ public interface ExpressionTransformer<T extends ExpressionTransformationContext
 	 * @param context will never be {@literal null}.
 	 * @return
 	 */
-	Object transform(T context);
+	@Nullable Object transform(T context);
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/LiteralNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/LiteralNode.java
index 030ef0d055..b276831860 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/LiteralNode.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/LiteralNode.java
@@ -17,6 +17,7 @@
 
 import java.util.Set;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.expression.spel.ExpressionState;
 import org.springframework.expression.spel.ast.BooleanLiteral;
 import org.springframework.expression.spel.ast.FloatLiteral;
@@ -26,7 +27,6 @@
 import org.springframework.expression.spel.ast.NullLiteral;
 import org.springframework.expression.spel.ast.RealLiteral;
 import org.springframework.expression.spel.ast.StringLiteral;
-import org.springframework.lang.Nullable;
 
 /**
  * A node representing a literal in an expression.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java
index 5f1b0c4309..db2801e649 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java
@@ -21,9 +21,9 @@
 import java.util.HashMap;
 import java.util.Map;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.expression.spel.ExpressionState;
 import org.springframework.expression.spel.ast.MethodReference;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 
@@ -272,8 +272,7 @@ public class MethodReferenceNode extends ExpressionNode {
 	 * @return can be {@literal null}.
 	 * @since 1.10
 	 */
-	@Nullable
-	public AggregationMethodReference getMethodReference() {
+	public @Nullable AggregationMethodReference getMethodReference() {
 
 		String name = getName();
 		String methodName = name.substring(0, name.indexOf('('));
@@ -288,7 +287,7 @@ public static final class AggregationMethodReference {
 
 		private final @Nullable String mongoOperator;
 		private final @Nullable ArgumentType argumentType;
-		private final @Nullable String[] argumentMap;
+		private final String @Nullable[] argumentMap;
 
 		/**
 		 * Creates new {@link AggregationMethodReference}.
@@ -298,7 +297,7 @@ public static final class AggregationMethodReference {
 		 * @param argumentMap can be {@literal null}.
 		 */
 		private AggregationMethodReference(@Nullable String mongoOperator, @Nullable ArgumentType argumentType,
-				@Nullable String[] argumentMap) {
+				String @Nullable[] argumentMap) {
 
 			this.mongoOperator = mongoOperator;
 			this.argumentType = argumentType;
@@ -310,8 +309,7 @@ private AggregationMethodReference(@Nullable String mongoOperator, @Nullable Arg
 		 *
 		 * @return can be {@literal null}.
 		 */
-		@Nullable
-		public String getMongoOperator() {
+		public @Nullable String getMongoOperator() {
 			return this.mongoOperator;
 		}
 
@@ -320,8 +318,7 @@ public String getMongoOperator() {
 		 *
 		 * @return never {@literal null}.
 		 */
-		@Nullable
-		public ArgumentType getArgumentType() {
+		public @Nullable ArgumentType getArgumentType() {
 			return this.argumentType;
 		}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/package-info.java
index fbfa2ae78b..627cb8a04f 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/package-info.java
@@ -3,6 +3,6 @@
  * 
  * @since 1.4
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 package org.springframework.data.mongodb.core.spel;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/CriteriaValidator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/CriteriaValidator.java
index 779ed4ec9f..d739994e89 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/CriteriaValidator.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/CriteriaValidator.java
@@ -16,10 +16,10 @@
 package org.springframework.data.mongodb.core.validation;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.query.Criteria;
 import org.springframework.data.mongodb.core.query.CriteriaDefinition;
 import org.springframework.data.mongodb.core.query.SerializationUtils;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/DocumentValidator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/DocumentValidator.java
index 5e27b99ad6..9adf5fcf79 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/DocumentValidator.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/DocumentValidator.java
@@ -16,8 +16,8 @@
 package org.springframework.data.mongodb.core.validation;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.query.SerializationUtils;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/JsonSchemaValidator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/JsonSchemaValidator.java
index 61ef8c5b4f..b04e65b1db 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/JsonSchemaValidator.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/JsonSchemaValidator.java
@@ -16,9 +16,9 @@
 package org.springframework.data.mongodb.core.validation;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.query.SerializationUtils;
 import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/package-info.java
index 002a4ee1fb..83fe5719a3 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/package-info.java
@@ -1,6 +1,6 @@
 /**
  * MongoDB schema validation specifics.
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 @org.springframework.lang.NonNullFields
 package org.springframework.data.mongodb.core.validation;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsCriteria.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsCriteria.java
index 54010a7c65..e0aa5bb2d5 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsCriteria.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsCriteria.java
@@ -15,8 +15,8 @@
  */
 package org.springframework.data.mongodb.gridfs;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.query.Criteria;
-import org.springframework.lang.Nullable;
 
 /**
  * GridFs-specific helper class to define {@link Criteria}s.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsObject.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsObject.java
index f73c0c943f..99807e63a2 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsObject.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsObject.java
@@ -16,9 +16,10 @@
 package org.springframework.data.mongodb.gridfs;
 
 import org.bson.Document;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 import com.mongodb.client.gridfs.model.GridFSFile;
+import org.springframework.lang.Contract;
 
 /**
  * A common interface when dealing with GridFs items using Spring Data.
@@ -110,6 +111,7 @@ public static Options from(@Nullable GridFSFile gridFSFile) {
 		 * @param contentType must not be {@literal null}.
 		 * @return new instance of {@link Options}.
 		 */
+		@Contract("_ -> new")
 		public Options contentType(String contentType) {
 
 			Options target = new Options(new Document(metadata), chunkSize);
@@ -121,6 +123,7 @@ public Options contentType(String contentType) {
 		 * @param metadata
 		 * @return new instance of {@link Options}.
 		 */
+		@Contract("_ -> new")
 		public Options metadata(Document metadata) {
 			return new Options(metadata, chunkSize);
 		}
@@ -129,6 +132,7 @@ public Options metadata(Document metadata) {
 		 * @param chunkSize the file chunk size to use.
 		 * @return new instance of {@link Options}.
 		 */
+		@Contract("_ -> new")
 		public Options chunkSize(int chunkSize) {
 			return new Options(metadata, chunkSize);
 		}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsOperations.java
index bf5a1d86e3..4878b431f4 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsOperations.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsOperations.java
@@ -19,11 +19,11 @@
 
 import org.bson.Document;
 import org.bson.types.ObjectId;
+import org.jspecify.annotations.Nullable;
 import org.springframework.core.io.support.ResourcePatternResolver;
 import org.springframework.data.domain.Sort;
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mongodb.gridfs.GridFsUpload.GridFsUploadBuilder;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
 
@@ -181,8 +181,7 @@ default ObjectId store(InputStream content, @Nullable String filename, @Nullable
 	 * @param query must not be {@literal null}.
 	 * @return can be {@literal null}.
 	 */
-	@Nullable
-	com.mongodb.client.gridfs.model.GridFSFile findOne(Query query);
+	com.mongodb.client.gridfs.model.@Nullable GridFSFile findOne(Query query);
 
 	/**
 	 * Deletes all files matching the given {@link Query}.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsOperationsSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsOperationsSupport.java
index b3d3771f3c..9a5621dcbc 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsOperationsSupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsOperationsSupport.java
@@ -18,9 +18,9 @@
 import java.util.Optional;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.convert.MongoConverter;
 import org.springframework.data.mongodb.core.convert.QueryMapper;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsResource.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsResource.java
index 0873432977..db6ce9833d 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsResource.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsResource.java
@@ -21,10 +21,10 @@
 import java.io.InputStream;
 import java.util.Optional;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.core.io.InputStreamResource;
 import org.springframework.core.io.Resource;
 import org.springframework.data.mongodb.util.BsonUtils;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
 import com.mongodb.MongoGridFSException;
@@ -105,6 +105,7 @@ public InputStream getInputStream() throws IOException, IllegalStateException {
 	}
 
 	@Override
+	@SuppressWarnings("NullAway")
 	public long contentLength() throws IOException {
 
 		verifyExists();
@@ -122,6 +123,7 @@ public boolean exists() {
 	}
 
 	@Override
+	@SuppressWarnings("NullAway")
 	public long lastModified() throws IOException {
 
 		verifyExists();
@@ -139,6 +141,7 @@ public String getDescription() {
 	 * @return never {@literal null}.
 	 * @throws IllegalStateException if the file does not {@link #exists()}.
 	 */
+	@SuppressWarnings("NullAway")
 	public Object getId() {
 
 		Assert.state(exists(), () -> String.format("%s does not exist.", getDescription()));
@@ -147,7 +150,8 @@ public Object getId() {
 	}
 
 	@Override
-	public Object getFileId() {
+	@SuppressWarnings("NullAway")
+	public @Nullable Object getFileId() {
 
 		Assert.state(exists(), () -> String.format("%s does not exist.", getDescription()));
 		return BsonUtils.toJavaType(getGridFSFile().getId());
@@ -157,8 +161,7 @@ public Object getFileId() {
 	 * @return the underlying {@link GridFSFile}. Can be {@literal null} if absent.
 	 * @since 2.2
 	 */
-	@Nullable
-	public GridFSFile getGridFSFile() {
+	public @Nullable GridFSFile getGridFSFile() {
 		return this.file;
 	}
 
@@ -170,6 +173,7 @@ public GridFSFile getGridFSFile() {
 	 *           provided via {@link GridFSFile}.
 	 * @throws IllegalStateException if the file does not {@link #exists()}.
 	 */
+	@SuppressWarnings("NullAway")
 	public String getContentType() {
 
 		Assert.state(exists(), () -> String.format("%s does not exist.", getDescription()));
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsTemplate.java
index 8187c7dbc3..722a57edc1 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsTemplate.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsTemplate.java
@@ -26,13 +26,13 @@
 
 import org.bson.Document;
 import org.bson.types.ObjectId;
+import org.jspecify.annotations.Nullable;
 import org.springframework.core.io.support.ResourcePatternResolver;
 import org.springframework.data.mongodb.MongoDatabaseFactory;
 import org.springframework.data.mongodb.core.convert.MongoConverter;
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mongodb.util.BsonUtils;
 import org.springframework.data.util.Lazy;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 
@@ -167,7 +167,7 @@ public void delete(Query query) {
 	}
 
 	@Override
-	public ClassLoader getClassLoader() {
+	public @Nullable ClassLoader getClassLoader() {
 		return null;
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsUpload.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsUpload.java
index 9f8d9a47d2..6f2b9ed85b 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsUpload.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsUpload.java
@@ -20,9 +20,9 @@
 
 import org.bson.Document;
 import org.bson.types.ObjectId;
-
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.util.Lazy;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 
 import com.mongodb.client.gridfs.model.GridFSFile;
@@ -61,8 +61,7 @@ private GridFsUpload(@Nullable ID id, Lazy<InputStream> dataStream, String filen
 	 * @see org.springframework.data.mongodb.gridfs.GridFsObject#getFileId()
 	 */
 	@Override
-	@Nullable
-	public ID getFileId() {
+	public @Nullable ID getFileId() {
 		return id;
 	}
 
@@ -72,6 +71,7 @@ public String getFilename() {
 	}
 
 	@Override
+	@SuppressWarnings("NullAway")
 	public InputStream getContent() {
 		return dataStream.orElse(InputStream.nullInputStream());
 	}
@@ -98,9 +98,9 @@ public static GridFsUploadBuilder<ObjectId> fromStream(InputStream stream) {
 	 */
 	public static class GridFsUploadBuilder<T> {
 
-		private Object id;
-		private Lazy<InputStream> dataStream;
-		private String filename;
+		private @Nullable Object id;
+		private @Nullable Lazy<InputStream> dataStream;
+		private @Nullable String filename;
 		private Options options = Options.none();
 
 		private GridFsUploadBuilder() {}
@@ -124,6 +124,7 @@ public GridFsUploadBuilder<T> content(InputStream stream) {
 		 * @param stream the upload content.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public GridFsUploadBuilder<T> content(Supplier<InputStream> stream) {
 
 			Assert.notNull(stream, "InputStream Supplier must not be null");
@@ -139,6 +140,8 @@ public GridFsUploadBuilder<T> content(Supplier<InputStream> stream) {
 		 * @param <T1>
 		 * @return this.
 		 */
+		@SuppressWarnings("unchecked")
+		@Contract("_ -> this")
 		public <T1> GridFsUploadBuilder<T1> id(T1 id) {
 
 			this.id = id;
@@ -151,6 +154,7 @@ public <T1> GridFsUploadBuilder<T1> id(T1 id) {
 		 * @param filename the filename to use.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public GridFsUploadBuilder<T> filename(String filename) {
 
 			this.filename = filename;
@@ -163,6 +167,7 @@ public GridFsUploadBuilder<T> filename(String filename) {
 		 * @param options must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public GridFsUploadBuilder<T> options(Options options) {
 
 			Assert.notNull(options, "Options must not be null");
@@ -177,6 +182,7 @@ public GridFsUploadBuilder<T> options(Options options) {
 		 * @param metadata must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public GridFsUploadBuilder<T> metadata(Document metadata) {
 
 			this.options = this.options.metadata(metadata);
@@ -189,6 +195,7 @@ public GridFsUploadBuilder<T> metadata(Document metadata) {
 		 * @param chunkSize use negative number for default.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public GridFsUploadBuilder<T> chunkSize(int chunkSize) {
 
 			this.options = this.options.chunkSize(chunkSize);
@@ -201,6 +208,7 @@ public GridFsUploadBuilder<T> chunkSize(int chunkSize) {
 		 * @param gridFSFile must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public GridFsUploadBuilder<T> gridFsFile(GridFSFile gridFSFile) {
 
 			Assert.notNull(gridFSFile, "GridFSFile must not be null");
@@ -219,13 +227,20 @@ public GridFsUploadBuilder<T> gridFsFile(GridFSFile gridFSFile) {
 		 * @param contentType must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public GridFsUploadBuilder<T> contentType(String contentType) {
 
 			this.options = this.options.contentType(contentType);
 			return this;
 		}
 
+		@Contract("-> new")
 		public GridFsUpload<T> build() {
+
+			Assert.notNull(dataStream, "DataStream must be set first");
+			Assert.notNull(filename, "Filename must be set first");
+			Assert.notNull(options, "Options must be set first");
+
 			return new GridFsUpload(id, dataStream, filename, options);
 		}
 	}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsOperations.java
index 9ee47e0bb9..f8a6bd804f 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsOperations.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsOperations.java
@@ -20,12 +20,12 @@
 
 import org.bson.Document;
 import org.bson.types.ObjectId;
+import org.jspecify.annotations.Nullable;
 import org.reactivestreams.Publisher;
 import org.springframework.core.io.buffer.DataBuffer;
 import org.springframework.data.domain.Sort;
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mongodb.gridfs.ReactiveGridFsUpload.ReactiveGridFsUploadBuilder;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsResource.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsResource.java
index aec7cadef1..e889ec7183 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsResource.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsResource.java
@@ -22,6 +22,7 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.bson.BsonValue;
+import org.jspecify.annotations.Nullable;
 import org.reactivestreams.Publisher;
 import org.springframework.core.io.Resource;
 import org.springframework.core.io.buffer.DataBuffer;
@@ -29,7 +30,6 @@
 import org.springframework.core.io.buffer.DataBufferUtils;
 import org.springframework.core.io.buffer.DefaultDataBufferFactory;
 import org.springframework.data.mongodb.util.BsonUtils;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
 import com.mongodb.client.gridfs.model.GridFSFile;
@@ -115,7 +115,7 @@ public static ReactiveGridFsResource absent(String filename) {
 	}
 
 	@Override
-	public Object getFileId() {
+	public @Nullable Object getFileId() {
 		return id instanceof BsonValue bsonValue ? BsonUtils.toJavaType(bsonValue) : id;
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsTemplate.java
index 305e55aee4..092f81d1fa 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsTemplate.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsTemplate.java
@@ -26,6 +26,7 @@
 import org.bson.BsonValue;
 import org.bson.Document;
 import org.bson.types.ObjectId;
+import org.jspecify.annotations.Nullable;
 import org.reactivestreams.Publisher;
 import org.springframework.core.io.buffer.DataBuffer;
 import org.springframework.core.io.buffer.DataBufferFactory;
@@ -37,7 +38,6 @@
 import org.springframework.data.mongodb.core.query.SerializationUtils;
 import org.springframework.data.mongodb.util.BsonUtils;
 import org.springframework.data.util.Lazy;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsUpload.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsUpload.java
index 2f16c3b06e..09ea77798c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsUpload.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsUpload.java
@@ -17,9 +17,10 @@
 
 import org.bson.Document;
 import org.bson.types.ObjectId;
+import org.jspecify.annotations.Nullable;
 import org.reactivestreams.Publisher;
 import org.springframework.core.io.buffer.DataBuffer;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 
 import com.mongodb.client.gridfs.model.GridFSFile;
@@ -58,8 +59,7 @@ private ReactiveGridFsUpload(@Nullable ID id, Publisher<DataBuffer> dataStream,
 	 * @see org.springframework.data.mongodb.gridfs.GridFsObject#getFileId()
 	 */
 	@Override
-	@Nullable
-	public ID getFileId() {
+	public @Nullable ID getFileId() {
 		return id;
 	}
 
@@ -96,8 +96,8 @@ public static ReactiveGridFsUploadBuilder<ObjectId> fromPublisher(Publisher<Data
 	public static class ReactiveGridFsUploadBuilder<T> {
 
 		private @Nullable Object id;
-		private Publisher<DataBuffer> dataStream;
-		private String filename;
+		private @Nullable Publisher<DataBuffer> dataStream;
+		private @Nullable String filename;
 		private Options options = Options.none();
 
 		private ReactiveGridFsUploadBuilder() {}
@@ -108,6 +108,7 @@ private ReactiveGridFsUploadBuilder() {}
 		 * @param source the upload content.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public ReactiveGridFsUploadBuilder<T> content(Publisher<DataBuffer> source) {
 			this.dataStream = source;
 			return this;
@@ -120,6 +121,7 @@ public ReactiveGridFsUploadBuilder<T> content(Publisher<DataBuffer> source) {
 		 * @param <T1>
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public <T1> ReactiveGridFsUploadBuilder<T1> id(T1 id) {
 
 			this.id = id;
@@ -132,6 +134,7 @@ public <T1> ReactiveGridFsUploadBuilder<T1> id(T1 id) {
 		 * @param filename the filename to use.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public ReactiveGridFsUploadBuilder<T> filename(String filename) {
 
 			this.filename = filename;
@@ -144,6 +147,7 @@ public ReactiveGridFsUploadBuilder<T> filename(String filename) {
 		 * @param options must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public ReactiveGridFsUploadBuilder<T> options(Options options) {
 
 			Assert.notNull(options, "Options must not be null");
@@ -156,8 +160,9 @@ public ReactiveGridFsUploadBuilder<T> options(Options options) {
 		 * Set the file metadata.
 		 *
 		 * @param metadata must not be {@literal null}.
-		 * @return
+		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public ReactiveGridFsUploadBuilder<T> metadata(Document metadata) {
 
 			this.options = this.options.metadata(metadata);
@@ -168,8 +173,9 @@ public ReactiveGridFsUploadBuilder<T> metadata(Document metadata) {
 		 * Set the upload chunk size in bytes.
 		 *
 		 * @param chunkSize use negative number for default.
-		 * @return
+		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public ReactiveGridFsUploadBuilder<T> chunkSize(int chunkSize) {
 
 			this.options = this.options.chunkSize(chunkSize);
@@ -182,6 +188,7 @@ public ReactiveGridFsUploadBuilder<T> chunkSize(int chunkSize) {
 		 * @param gridFSFile must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public ReactiveGridFsUploadBuilder<T> gridFsFile(GridFSFile gridFSFile) {
 
 			Assert.notNull(gridFSFile, "GridFSFile must not be null");
@@ -200,13 +207,20 @@ public ReactiveGridFsUploadBuilder<T> gridFsFile(GridFSFile gridFSFile) {
 		 * @param contentType must not be {@literal null}.
 		 * @return this.
 		 */
+		@Contract("_ -> this")
 		public ReactiveGridFsUploadBuilder<T> contentType(String contentType) {
 
 			this.options = this.options.contentType(contentType);
 			return this;
 		}
 
+		@Contract("-> new")
 		public ReactiveGridFsUpload<T> build() {
+
+			Assert.notNull(dataStream, "DataStream must be set first");
+			Assert.notNull(filename, "Filename must be set first");
+			Assert.notNull(options, "Options must be set first");
+
 			return new ReactiveGridFsUpload(id, dataStream, filename, options);
 		}
 	}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/package-info.java
index 2f3b5af150..57726d69cc 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/package-info.java
@@ -1,6 +1,6 @@
 /**
  * Support for MongoDB GridFS feature.
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 package org.springframework.data.mongodb.gridfs;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/AbstractMonitor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/AbstractMonitor.java
deleted file mode 100644
index 5ffe37a4a7..0000000000
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/AbstractMonitor.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright 2002-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.mongodb.monitor;
-
-import java.util.List;
-import java.util.stream.Collectors;
-
-import org.bson.Document;
-
-import com.mongodb.ServerAddress;
-import com.mongodb.client.MongoClient;
-import com.mongodb.client.MongoDatabase;
-import com.mongodb.connection.ServerDescription;
-
-/**
- * Base class to encapsulate common configuration settings when connecting to a database
- *
- * @author Mark Pollack
- * @author Oliver Gierke
- * @author Christoph Strobl
- * @deprecated since 4.5
- */
-@Deprecated(since = "4.5", forRemoval = true)
-public abstract class AbstractMonitor {
-
-	private final MongoClient mongoClient;
-
-	/**
-	 * @param mongoClient must not be {@literal null}.
-	 * @since 2.2
-	 */
-	protected AbstractMonitor(MongoClient mongoClient) {
-		this.mongoClient = mongoClient;
-	}
-
-	public Document getServerStatus() {
-		return getDb("admin").runCommand(new Document("serverStatus", 1).append("rangeDeleter", 1).append("repl", 1));
-	}
-
-	public MongoDatabase getDb(String databaseName) {
-		return mongoClient.getDatabase(databaseName);
-	}
-
-	protected MongoClient getMongoClient() {
-		return mongoClient;
-	}
-
-	protected List<ServerAddress> hosts() {
-
-		return mongoClient.getClusterDescription().getServerDescriptions().stream().map(ServerDescription::getAddress)
-				.collect(Collectors.toList());
-	}
-}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/AssertMetrics.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/AssertMetrics.java
deleted file mode 100644
index 15666fa4d0..0000000000
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/AssertMetrics.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright 2002-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.mongodb.monitor;
-
-import org.bson.Document;
-import org.springframework.jmx.export.annotation.ManagedMetric;
-import org.springframework.jmx.export.annotation.ManagedResource;
-import org.springframework.jmx.support.MetricType;
-
-import com.mongodb.client.MongoClient;
-
-/**
- * JMX Metrics for assertions
- *
- * @author Mark Pollack
- * @deprecated since 4.5
- */
-@Deprecated(since = "4.5", forRemoval = true)
-@ManagedResource(description = "Assertion Metrics")
-public class AssertMetrics extends AbstractMonitor {
-
-	/**
-	 * @param mongoClient must not be {@literal null}.
-	 * @since 2.2
-	 */
-	public AssertMetrics(MongoClient mongoClient) {
-		super(mongoClient);
-	}
-
-	@ManagedMetric(metricType = MetricType.COUNTER, displayName = "Regular")
-	public int getRegular() {
-		return getBtree("regular");
-	}
-
-	@ManagedMetric(metricType = MetricType.COUNTER, displayName = "Warning")
-	public int getWarning() {
-		return getBtree("warning");
-	}
-
-	@ManagedMetric(metricType = MetricType.COUNTER, displayName = "Msg")
-	public int getMsg() {
-		return getBtree("msg");
-	}
-
-	@ManagedMetric(metricType = MetricType.COUNTER, displayName = "User")
-	public int getUser() {
-		return getBtree("user");
-	}
-
-	@ManagedMetric(metricType = MetricType.GAUGE, displayName = "Rollovers")
-	public int getRollovers() {
-		return getBtree("rollovers");
-	}
-
-	private int getBtree(String key) {
-		Document asserts = (Document) getServerStatus().get("asserts");
-		// Class c = btree.get(key).getClass();
-		return (Integer) asserts.get(key);
-	}
-
-}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/BackgroundFlushingMetrics.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/BackgroundFlushingMetrics.java
deleted file mode 100644
index 2ceb75a4f8..0000000000
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/BackgroundFlushingMetrics.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright 2002-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.mongodb.monitor;
-
-import java.util.Date;
-
-import org.bson.Document;
-import org.springframework.jmx.export.annotation.ManagedMetric;
-import org.springframework.jmx.export.annotation.ManagedResource;
-import org.springframework.jmx.support.MetricType;
-
-import com.mongodb.client.MongoClient;
-
-/**
- * JMX Metrics for Background Flushing
- *
- * @author Mark Pollack
- * @deprecated since 4.5
- */
-@Deprecated(since = "4.5", forRemoval = true)
-@ManagedResource(description = "Background Flushing Metrics")
-public class BackgroundFlushingMetrics extends AbstractMonitor {
-
-	/**
-	 * @param mongoClient must not be {@literal null}.
-	 * @since 2.2
-	 */
-	public BackgroundFlushingMetrics(MongoClient mongoClient) {
-		super(mongoClient);
-	}
-
-	@ManagedMetric(metricType = MetricType.COUNTER, displayName = "Flushes")
-	public int getFlushes() {
-		return getFlushingData("flushes", java.lang.Integer.class);
-	}
-
-	@ManagedMetric(metricType = MetricType.COUNTER, displayName = "Total ms", unit = "ms")
-	public int getTotalMs() {
-		return getFlushingData("total_ms", java.lang.Integer.class);
-	}
-
-	@ManagedMetric(metricType = MetricType.GAUGE, displayName = "Average ms", unit = "ms")
-	public double getAverageMs() {
-		return getFlushingData("average_ms", java.lang.Double.class);
-	}
-
-	@ManagedMetric(metricType = MetricType.GAUGE, displayName = "Last Ms", unit = "ms")
-	public int getLastMs() {
-		return getFlushingData("last_ms", java.lang.Integer.class);
-	}
-
-	@ManagedMetric(metricType = MetricType.GAUGE, displayName = "Last finished")
-	public Date getLastFinished() {
-		return getLast();
-	}
-
-	@SuppressWarnings("unchecked")
-	private <T> T getFlushingData(String key, Class<T> targetClass) {
-		Document mem = (Document) getServerStatus().get("backgroundFlushing");
-		return (T) mem.get(key);
-	}
-
-	private Date getLast() {
-		Document bgFlush = (Document) getServerStatus().get("backgroundFlushing");
-		Date lastFinished = (Date) bgFlush.get("last_finished");
-		return lastFinished;
-	}
-
-}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/BtreeIndexCounters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/BtreeIndexCounters.java
deleted file mode 100644
index 671d017e05..0000000000
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/BtreeIndexCounters.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright 2002-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.mongodb.monitor;
-
-import org.bson.Document;
-import org.springframework.jmx.export.annotation.ManagedMetric;
-import org.springframework.jmx.export.annotation.ManagedResource;
-import org.springframework.jmx.support.MetricType;
-
-import com.mongodb.client.MongoClient;
-
-/**
- * JMX Metrics for B-tree index counters
- *
- * @author Mark Pollack
- * @deprecated since 4.5
- */
-@Deprecated(since = "4.5", forRemoval = true)
-@ManagedResource(description = "Btree Metrics")
-public class BtreeIndexCounters extends AbstractMonitor {
-
-	/**
-	 * @param mongoClient must not be {@literal null}.
-	 * @since 2.2
-	 */
-	public BtreeIndexCounters(MongoClient mongoClient) {
-		super(mongoClient);
-	}
-
-	@ManagedMetric(metricType = MetricType.GAUGE, displayName = "Accesses")
-	public int getAccesses() {
-		return getBtree("accesses");
-	}
-
-	@ManagedMetric(metricType = MetricType.GAUGE, displayName = "Hits")
-	public int getHits() {
-		return getBtree("hits");
-	}
-
-	@ManagedMetric(metricType = MetricType.GAUGE, displayName = "Misses")
-	public int getMisses() {
-		return getBtree("misses");
-	}
-
-	@ManagedMetric(metricType = MetricType.GAUGE, displayName = "Resets")
-	public int getResets() {
-		return getBtree("resets");
-	}
-
-	@ManagedMetric(metricType = MetricType.GAUGE, displayName = "Miss Ratio")
-	public int getMissRatio() {
-		return getBtree("missRatio");
-	}
-
-	private int getBtree(String key) {
-		Document indexCounters = (Document) getServerStatus().get("indexCounters");
-		if (indexCounters.get("note") != null) {
-			String message = (String) indexCounters.get("note");
-			if (message.contains("not supported")) {
-				return -1;
-			}
-		}
-		Document btree = (Document) indexCounters.get("btree");
-		// Class c = btree.get(key).getClass();
-		return (Integer) btree.get(key);
-	}
-
-}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/ConnectionMetrics.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/ConnectionMetrics.java
deleted file mode 100644
index 0d0eb84b35..0000000000
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/ConnectionMetrics.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright 2002-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.mongodb.monitor;
-
-import org.bson.Document;
-import org.springframework.jmx.export.annotation.ManagedMetric;
-import org.springframework.jmx.export.annotation.ManagedResource;
-import org.springframework.jmx.support.MetricType;
-
-import com.mongodb.client.MongoClient;
-
-/**
- * JMX Metrics for Connections
- *
- * @author Mark Pollack
- * @deprecated since 4.5
- */
-@Deprecated(since = "4.5", forRemoval = true)
-@ManagedResource(description = "Connection metrics")
-public class ConnectionMetrics extends AbstractMonitor {
-
-	/**
-	 * @param mongoClient must not be {@literal null}.
-	 * @since 2.2
-	 */
-	public ConnectionMetrics(MongoClient mongoClient) {
-		super(mongoClient);
-	}
-
-	@ManagedMetric(metricType = MetricType.GAUGE, displayName = "Current Connections")
-	public int getCurrent() {
-		return getConnectionData("current", java.lang.Integer.class);
-	}
-
-	@ManagedMetric(metricType = MetricType.GAUGE, displayName = "Available Connections")
-	public int getAvailable() {
-		return getConnectionData("available", java.lang.Integer.class);
-	}
-
-	@SuppressWarnings("unchecked")
-	private <T> T getConnectionData(String key, Class<T> targetClass) {
-		Document mem = (Document) getServerStatus().get("connections");
-		// Class c = mem.get(key).getClass();
-		return (T) mem.get(key);
-	}
-
-}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/GlobalLockMetrics.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/GlobalLockMetrics.java
deleted file mode 100644
index 6997f5fba8..0000000000
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/GlobalLockMetrics.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright 2002-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.mongodb.monitor;
-
-import org.bson.Document;
-import org.springframework.jmx.export.annotation.ManagedMetric;
-import org.springframework.jmx.export.annotation.ManagedResource;
-import org.springframework.jmx.support.MetricType;
-
-import com.mongodb.DBObject;
-import com.mongodb.client.MongoClient;
-
-/**
- * JMX Metrics for Global Locks
- *
- * @author Mark Pollack
- * @deprecated since 4.5
- */
-@Deprecated(since = "4.5", forRemoval = true)
-@ManagedResource(description = "Global Lock Metrics")
-public class GlobalLockMetrics extends AbstractMonitor {
-
-	/**
-	 * @param mongoClient must not be {@literal null}.
-	 * @since 2.2
-	 */
-	public GlobalLockMetrics(MongoClient mongoClient) {
-		super(mongoClient);
-	}
-
-	@ManagedMetric(metricType = MetricType.COUNTER, displayName = "Total time")
-	public double getTotalTime() {
-		return getGlobalLockData("totalTime", java.lang.Double.class);
-	}
-
-	@ManagedMetric(metricType = MetricType.COUNTER, displayName = "Lock time", unit = "s")
-	public double getLockTime() {
-		return getGlobalLockData("lockTime", java.lang.Double.class);
-	}
-
-	@ManagedMetric(metricType = MetricType.GAUGE, displayName = "Lock time")
-	public double getLockTimeRatio() {
-		return getGlobalLockData("ratio", java.lang.Double.class);
-	}
-
-	@ManagedMetric(metricType = MetricType.GAUGE, displayName = "Current Queue")
-	public int getCurrentQueueTotal() {
-		return getCurrentQueue("total");
-	}
-
-	@ManagedMetric(metricType = MetricType.GAUGE, displayName = "Reader Queue")
-	public int getCurrentQueueReaders() {
-		return getCurrentQueue("readers");
-	}
-
-	@ManagedMetric(metricType = MetricType.GAUGE, displayName = "Writer Queue")
-	public int getCurrentQueueWriters() {
-		return getCurrentQueue("writers");
-	}
-
-	@SuppressWarnings("unchecked")
-	private <T> T getGlobalLockData(String key, Class<T> targetClass) {
-		DBObject globalLock = (DBObject) getServerStatus().get("globalLock");
-		return (T) globalLock.get(key);
-	}
-
-	private int getCurrentQueue(String key) {
-		Document globalLock = (Document) getServerStatus().get("globalLock");
-		Document currentQueue = (Document) globalLock.get("currentQueue");
-		return (Integer) currentQueue.get(key);
-	}
-}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/MemoryMetrics.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/MemoryMetrics.java
deleted file mode 100644
index 4dbdebb26f..0000000000
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/MemoryMetrics.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright 2002-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.mongodb.monitor;
-
-import org.bson.Document;
-import org.springframework.jmx.export.annotation.ManagedMetric;
-import org.springframework.jmx.export.annotation.ManagedResource;
-import org.springframework.jmx.support.MetricType;
-
-import com.mongodb.client.MongoClient;
-
-/**
- * JMX Metrics for Memory
- *
- * @author Mark Pollack
- * @deprecated since 4.5
- */
-@Deprecated(since = "4.5", forRemoval = true)
-@ManagedResource(description = "Memory Metrics")
-public class MemoryMetrics extends AbstractMonitor {
-
-	/**
-	 * @param mongoClient
-	 * @since 2.2
-	 */
-	public MemoryMetrics(MongoClient mongoClient) {
-		super(mongoClient);
-	}
-
-	@ManagedMetric(metricType = MetricType.COUNTER, displayName = "Memory address size")
-	public int getBits() {
-		return getMemData("bits", java.lang.Integer.class);
-	}
-
-	@ManagedMetric(metricType = MetricType.GAUGE, displayName = "Resident in Physical Memory", unit = "MB")
-	public int getResidentSpace() {
-		return getMemData("resident", java.lang.Integer.class);
-	}
-
-	@ManagedMetric(metricType = MetricType.GAUGE, displayName = "Virtual Address Space", unit = "MB")
-	public int getVirtualAddressSpace() {
-		return getMemData("virtual", java.lang.Integer.class);
-	}
-
-	@ManagedMetric(metricType = MetricType.GAUGE, displayName = "Is memory info supported on this platform")
-	public boolean getMemoryInfoSupported() {
-		return getMemData("supported", java.lang.Boolean.class);
-	}
-
-	@ManagedMetric(metricType = MetricType.GAUGE, displayName = "Memory Mapped Space", unit = "MB")
-	public int getMemoryMappedSpace() {
-		return getMemData("mapped", java.lang.Integer.class);
-	}
-
-	@SuppressWarnings("unchecked")
-	private <T> T getMemData(String key, Class<T> targetClass) {
-		Document mem = (Document) getServerStatus().get("mem");
-		// Class c = mem.get(key).getClass();
-		return (T) mem.get(key);
-	}
-
-}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/OperationCounters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/OperationCounters.java
deleted file mode 100644
index 1624501490..0000000000
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/OperationCounters.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright 2002-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.mongodb.monitor;
-
-import org.bson.Document;
-import org.springframework.jmx.export.annotation.ManagedMetric;
-import org.springframework.jmx.export.annotation.ManagedResource;
-import org.springframework.jmx.support.MetricType;
-import org.springframework.util.NumberUtils;
-
-import com.mongodb.client.MongoClient;
-
-/**
- * JMX Metrics for Operation counters
- *
- * @author Mark Pollack
- * @deprecated since 4.5
- */
-@Deprecated(since = "4.5", forRemoval = true)
-@ManagedResource(description = "Operation Counters")
-public class OperationCounters extends AbstractMonitor {
-
-	/**
-	 * @param mongoClient
-	 * @since 2.2
-	 */
-	public OperationCounters(MongoClient mongoClient) {
-		super(mongoClient);
-	}
-
-	@ManagedMetric(metricType = MetricType.COUNTER, displayName = "Insert operation count")
-	public int getInsertCount() {
-		return getOpCounter("insert");
-	}
-
-	@ManagedMetric(metricType = MetricType.COUNTER, displayName = "Query operation count")
-	public int getQueryCount() {
-		return getOpCounter("query");
-	}
-
-	@ManagedMetric(metricType = MetricType.COUNTER, displayName = "Update operation count")
-	public int getUpdateCount() {
-		return getOpCounter("update");
-	}
-
-	@ManagedMetric(metricType = MetricType.COUNTER, displayName = "Delete operation count")
-	public int getDeleteCount() {
-		return getOpCounter("delete");
-	}
-
-	@ManagedMetric(metricType = MetricType.COUNTER, displayName = "GetMore operation count")
-	public int getGetMoreCount() {
-		return getOpCounter("getmore");
-	}
-
-	@ManagedMetric(metricType = MetricType.COUNTER, displayName = "Command operation count")
-	public int getCommandCount() {
-		return getOpCounter("command");
-	}
-
-	private int getOpCounter(String key) {
-		Document opCounters = (Document) getServerStatus().get("opcounters");
-		return NumberUtils.convertNumberToTargetClass((Number) opCounters.get(key), Integer.class);
-	}
-}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/ServerInfo.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/ServerInfo.java
deleted file mode 100644
index 3aedf3f29f..0000000000
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/ServerInfo.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright 2012-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.mongodb.monitor;
-
-import java.net.UnknownHostException;
-
-import org.springframework.jmx.export.annotation.ManagedMetric;
-import org.springframework.jmx.export.annotation.ManagedOperation;
-import org.springframework.jmx.export.annotation.ManagedResource;
-import org.springframework.jmx.support.MetricType;
-import org.springframework.util.StringUtils;
-
-import com.mongodb.client.MongoClient;
-
-/**
- * Expose basic server information via JMX
- *
- * @author Mark Pollack
- * @author Thomas Darimont
- * @author Christoph Strobl
- * @deprecated since 4.5
- */
-@Deprecated(since = "4.5", forRemoval = true)
-@ManagedResource(description = "Server Information")
-public class ServerInfo extends AbstractMonitor {
-
-	/**
-	 * @param mongoClient
-	 * @since 2.2
-	 */
-	protected ServerInfo(MongoClient mongoClient) {
-		super(mongoClient);
-	}
-
-	/**
-	 * Returns the hostname of the used server reported by MongoDB.
-	 *
-	 * @return the reported hostname can also be an IP address.
-	 * @throws UnknownHostException
-	 */
-	@ManagedOperation(description = "Server host name")
-	public String getHostName() throws UnknownHostException {
-
-		/*
-		 * UnknownHostException is not necessary anymore, but clients could have
-		 * called this method in a try..catch(UnknownHostException) already
-		 */
-		return StringUtils.collectionToDelimitedString(hosts(), ",");
-	}
-
-	@ManagedMetric(displayName = "Uptime Estimate")
-	public double getUptimeEstimate() {
-		return (Double) getServerStatus().get("uptimeEstimate");
-	}
-
-	@ManagedOperation(description = "MongoDB Server Version")
-	public String getVersion() {
-		return (String) getServerStatus().get("version");
-	}
-
-	@ManagedOperation(description = "Local Time")
-	public String getLocalTime() {
-		return (String) getServerStatus().get("localTime");
-	}
-
-	@ManagedMetric(metricType = MetricType.COUNTER, displayName = "Server uptime in seconds", unit = "seconds")
-	public double getUptime() {
-		return (Double) getServerStatus().get("uptime");
-	}
-}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/package-info.java
deleted file mode 100644
index 1e1c221b64..0000000000
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/package-info.java
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * MongoDB specific JMX monitoring support.
- */
-@Deprecated(since = "4.5", forRemoval = true)
-@org.springframework.lang.NonNullApi
-package org.springframework.data.mongodb.monitor;
-
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java
index b823ce223b..550a71b301 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java
@@ -17,10 +17,8 @@
 
 import io.micrometer.common.KeyValues;
 
-import java.net.InetSocketAddress;
-
 import org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandKeyNames;
-import org.springframework.data.mongodb.util.MongoCompatibilityAdapter;
+import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 
 import com.mongodb.ConnectionString;
@@ -67,6 +65,10 @@ public KeyValues getLowCardinalityKeyValues(MongoHandlerContext context) {
 					.and(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.withValue(context.getCollectionName()));
 		}
 
+		if(context.getCommandStartedEvent() == null) {
+			throw new IllegalStateException("not command started event present");
+		}
+
 		ConnectionDescription connectionDescription = context.getCommandStartedEvent().getConnectionDescription();
 
 		if (connectionDescription != null) {
@@ -78,16 +80,6 @@ public KeyValues getLowCardinalityKeyValues(MongoHandlerContext context) {
 				keyValues = keyValues.and(LowCardinalityCommandKeyNames.NET_TRANSPORT.withValue("IP.TCP"),
 						LowCardinalityCommandKeyNames.NET_PEER_NAME.withValue(serverAddress.getHost()),
 						LowCardinalityCommandKeyNames.NET_PEER_PORT.withValue("" + serverAddress.getPort()));
-
-				InetSocketAddress socketAddress = MongoCompatibilityAdapter.serverAddressAdapter(serverAddress)
-						.getSocketAddress();
-
-				if (socketAddress != null) {
-
-					keyValues = keyValues.and(
-							LowCardinalityCommandKeyNames.NET_SOCK_PEER_ADDR.withValue(socketAddress.getHostName()),
-							LowCardinalityCommandKeyNames.NET_SOCK_PEER_PORT.withValue("" + socketAddress.getPort()));
-				}
 			}
 
 			ConnectionId connectionId = connectionDescription.getConnectionId();
@@ -111,6 +103,8 @@ public String getContextualName(MongoHandlerContext context) {
 		String collectionName = context.getCollectionName();
 		CommandStartedEvent commandStartedEvent = context.getCommandStartedEvent();
 
+		Assert.notNull(commandStartedEvent, "CommandStartedEvent must not be null");
+
 		if (ObjectUtils.isEmpty(collectionName)) {
 			return commandStartedEvent.getCommandName();
 		}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MapRequestContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MapRequestContext.java
index 854e1481fc..6185c95db5 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MapRequestContext.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MapRequestContext.java
@@ -17,9 +17,11 @@
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.NoSuchElementException;
 import java.util.stream.Stream;
 
 import com.mongodb.RequestContext;
+import org.jspecify.annotations.Nullable;
 
 /**
  * A {@link Map}-based {@link RequestContext}.
@@ -42,7 +44,13 @@ public MapRequestContext(Map<Object, Object> context) {
 
 	@Override
 	public <T> T get(Object key) {
-		return (T) map.get(key);
+
+
+		T value = (T) map.get(key);
+		if(value != null) {
+			return value;
+		}
+		throw new NoSuchElementException("%s is missing".formatted(key));
 	}
 
 	@Override
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerContext.java
index cc58aac56e..cab9cd5cb8 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerContext.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerContext.java
@@ -25,8 +25,7 @@
 
 import org.bson.BsonDocument;
 import org.bson.BsonValue;
-
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 import com.mongodb.ConnectionString;
 import com.mongodb.RequestContext;
@@ -55,12 +54,12 @@ public class MongoHandlerContext extends SenderContext<Object> {
 					"killCursors", "listIndexes", "reIndex"));
 
 	private final @Nullable ConnectionString connectionString;
-	private final CommandStartedEvent commandStartedEvent;
-	private final RequestContext requestContext;
-	private final String collectionName;
+	private final @Nullable CommandStartedEvent commandStartedEvent;
+	private final @Nullable RequestContext requestContext;
+	private final @Nullable String collectionName;
 
-	private CommandSucceededEvent commandSucceededEvent;
-	private CommandFailedEvent commandFailedEvent;
+	private @Nullable CommandSucceededEvent commandSucceededEvent;
+	private @Nullable CommandFailedEvent commandFailedEvent;
 
 	public MongoHandlerContext(@Nullable ConnectionString connectionString, CommandStartedEvent commandStartedEvent,
 			RequestContext requestContext) {
@@ -72,28 +71,27 @@ public MongoHandlerContext(@Nullable ConnectionString connectionString, CommandS
 		this.collectionName = getCollectionName(commandStartedEvent);
 	}
 
-	public CommandStartedEvent getCommandStartedEvent() {
+	public @Nullable CommandStartedEvent getCommandStartedEvent() {
 		return this.commandStartedEvent;
 	}
 
-	public RequestContext getRequestContext() {
+	public @Nullable RequestContext getRequestContext() {
 		return this.requestContext;
 	}
 
 	public String getDatabaseName() {
-		return commandStartedEvent.getDatabaseName();
+		return commandStartedEvent != null ? commandStartedEvent.getDatabaseName() : "n/a";
 	}
 
-	public String getCollectionName() {
+	public @Nullable String getCollectionName() {
 		return this.collectionName;
 	}
 
 	public String getCommandName() {
-		return commandStartedEvent.getCommandName();
+		return commandStartedEvent != null ? commandStartedEvent.getCommandName() : "n/a";
 	}
 
-	@Nullable
-	public ConnectionString getConnectionString() {
+	public @Nullable ConnectionString getConnectionString() {
 		return connectionString;
 	}
 
@@ -135,8 +133,7 @@ private static String getCollectionName(CommandStartedEvent event) {
 	 *
 	 * @return trimmed string from {@code bsonValue} or null if the trimmed string was empty or the value wasn't a string
 	 */
-	@Nullable
-	private static String getNonEmptyBsonString(@Nullable BsonValue bsonValue) {
+	private static @Nullable String getNonEmptyBsonString(@Nullable BsonValue bsonValue) {
 
 		if (bsonValue == null || !bsonValue.isString()) {
 			return null;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservationCommandListener.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservationCommandListener.java
index 9360a95de2..914396ab96 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservationCommandListener.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservationCommandListener.java
@@ -23,7 +23,7 @@
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.Assert;
 
 import com.mongodb.ConnectionString;
@@ -197,8 +197,7 @@ private void doInObservation(@Nullable RequestContext requestContext,
 	 * @param context
 	 * @return
 	 */
-	@Nullable
-	private static Observation observationFromContext(RequestContext context) {
+	private static @Nullable Observation observationFromContext(RequestContext context) {
 
 		Observation observation = context.getOrDefault(ObservationThreadLocalAccessor.KEY, null);
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/package-info.java
index d240e12f9e..d6319e5f4f 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/package-info.java
@@ -1,5 +1,5 @@
 /**
  * Infrastructure to provide driver observability using Micrometer.
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 package org.springframework.data.mongodb.observability;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/package-info.java
index 900342bbcb..989655f4a6 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/package-info.java
@@ -1,5 +1,5 @@
 /**
  * Spring Data's MongoDB abstraction.
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 package org.springframework.data.mongodb;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/VectorSearch.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/VectorSearch.java
new file mode 100644
index 0000000000..336889f719
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/VectorSearch.java
@@ -0,0 +1,123 @@
+/*
+ * 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.mongodb.repository;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.core.annotation.AliasFor;
+import org.springframework.data.mongodb.core.aggregation.VectorSearchOperation;
+
+/**
+ * Annotation to declare Vector Search queries directly on repository methods. Vector Search queries are used to search
+ * for similar documents based on vector embeddings typically returning
+ * {@link org.springframework.data.domain.SearchResults} and limited by either a
+ * {@link org.springframework.data.domain.Score} (within) or a {@link org.springframework.data.domain.Range} of scores
+ * (between).
+ * <p>
+ * Vector search must define an index name using the {@link #indexName()} attribute. The index must be created in the
+ * MongoDB Atlas cluster before executing the query. Any misspelling of the index name will result in returning no
+ * results.
+ * <p>
+ * When using pre-filters, you can either define {@link #filter()} or use query derivation to define the pre-filter.
+ * {@link org.springframework.data.domain.Vector} and distance parameters are considered once these are present. Vector
+ * search supports sorting and will consider {@link org.springframework.data.domain.Sort} parameters.
+ *
+ * @author Mark Paluch
+ * @since 5.0
+ * @see org.springframework.data.domain.Score
+ * @see org.springframework.data.domain.Vector
+ * @see org.springframework.data.domain.SearchResults
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
+@Documented
+@Query
+@Hint
+public @interface VectorSearch {
+
+	/**
+	 * Configuration whether to use
+	 * {@link org.springframework.data.mongodb.core.aggregation.VectorSearchOperation.SearchType#ANN} or
+	 * {@link org.springframework.data.mongodb.core.aggregation.VectorSearchOperation.SearchType#ENN} for the search.
+	 *
+	 * @return the search type to use.
+	 */
+	VectorSearchOperation.SearchType searchType() default VectorSearchOperation.SearchType.DEFAULT;
+
+	/**
+	 * Name of the Atlas Vector Search index to use. Atlas Vector Search doesn't return results if you misspell the index
+	 * name or if the specified index doesn't already exist on the cluster.
+	 *
+	 * @return name of the Atlas Vector Search index to use.
+	 */
+	@AliasFor(annotation = Hint.class, value = "indexName")
+	String indexName();
+
+	/**
+	 * Indexed vector type field to search. This is defaulted from the domain model using the first Vector property found.
+	 *
+	 * @return an empty String by default.
+	 */
+	String path() default "";
+
+	/**
+	 * Takes a MongoDB JSON (MQL) string defining the pre-filter against indexed fields. Supports Value Expressions. Alias
+	 * for {@link VectorSearch#filter}.
+	 *
+	 * @return an empty String by default.
+	 */
+	@AliasFor(annotation = Query.class)
+	String value() default "";
+
+	/**
+	 * Takes a MongoDB JSON (MQL) string defining the pre-filter against indexed fields. Supports Value Expressions. Alias
+	 * for {@link VectorSearch#value}.
+	 *
+	 * @return an empty String by default.
+	 */
+	@AliasFor(annotation = Query.class, value = "value")
+	String filter() default "";
+
+	/**
+	 * Number of documents to return in the results. This value can't exceed the value of {@link #numCandidates} if you
+	 * specify {@link #numCandidates}. Limit accepts Value Expressions. A Vector Search method cannot define both,
+	 * {@code limit()} and a {@link org.springframework.data.domain.Limit} parameter. Supports Value Expressions.
+	 *
+	 * @return number of documents to return in the results.
+	 */
+	String limit() default "";
+
+	/**
+	 * Number of nearest neighbors to use during the search. Value must be less than or equal to ({@code <=})
+	 * {@code 10000}. You can't specify a number less than the {@link #limit() number of documents to return}. We
+	 * recommend that you specify a number at least {@code 20} times higher than the {@link #limit() number of documents
+	 * to return} to increase accuracy.
+	 * <p>
+	 * This over-request pattern is the recommended way to trade off latency and recall in your ANN searches, and we
+	 * recommend tuning this parameter based on your specific dataset size and query requirements. Required if the query
+	 * uses
+	 * {@link org.springframework.data.mongodb.core.aggregation.VectorSearchOperation.SearchType#ANN}/{@link org.springframework.data.mongodb.core.aggregation.VectorSearchOperation.SearchType#DEFAULT}.
+	 * Supports Value Expressions.
+	 *
+	 * @return number of nearest neighbors to use during the search.
+	 */
+	String numCandidates() default "";
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationInteraction.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationInteraction.java
new file mode 100644
index 0000000000..003982daf6
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationInteraction.java
@@ -0,0 +1,56 @@
+/*
+ * 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.mongodb.repository.aot;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.data.repository.aot.generate.QueryMetadata;
+
+/**
+ * An {@link MongoInteraction aggregation interaction}.
+ * 
+ * @author Christoph Strobl
+ * @since 5.0
+ */
+class AggregationInteraction extends MongoInteraction implements QueryMetadata {
+
+	private final StringAggregation aggregation;
+
+	AggregationInteraction(String[] raw) {
+		this.aggregation = new StringAggregation(raw);
+	}
+
+	List<String> stages() {
+		return Arrays.asList(aggregation.pipeline());
+	}
+
+	@Override
+	InteractionType getExecutionType() {
+		return InteractionType.AGGREGATION;
+	}
+
+	@Override
+	public Map<String, Object> serialize() {
+
+		return Map.of(pipelineSerializationKey(), stages());
+	}
+
+	protected String pipelineSerializationKey() {
+		return "pipeline";
+	}
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationUpdateInteraction.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationUpdateInteraction.java
new file mode 100644
index 0000000000..cc672ed1e9
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationUpdateInteraction.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 org.springframework.data.mongodb.repository.aot;
+
+import java.util.Map;
+
+/**
+ * An {@link MongoInteraction} to execute an aggregation update.
+ *
+ * @author Christoph Strobl
+ * @since 5.0
+ */
+class AggregationUpdateInteraction extends AggregationInteraction {
+
+	private final QueryInteraction filter;
+
+	AggregationUpdateInteraction(QueryInteraction filter, String[] raw) {
+
+		super(raw);
+		this.filter = filter;
+	}
+
+	QueryInteraction getFilter() {
+		return filter;
+	}
+
+	@Override
+	public Map<String, Object> serialize() {
+
+		Map<String, Object> serialized = filter.serialize();
+		serialized.putAll(super.serialize());
+		return serialized;
+	}
+
+	@Override
+	protected String pipelineSerializationKey() {
+		return "update-" + super.pipelineSerializationKey();
+	}
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotMongoRepositoryPostProcessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotMongoRepositoryPostProcessor.java
index d49726f724..324871b475 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotMongoRepositoryPostProcessor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotMongoRepositoryPostProcessor.java
@@ -15,9 +15,12 @@
  */
 package org.springframework.data.mongodb.repository.aot;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.aot.generate.GenerationContext;
+import org.springframework.data.aot.AotContext;
 import org.springframework.data.mongodb.aot.LazyLoadingProxyAotProcessor;
 import org.springframework.data.mongodb.aot.MongoAotPredicates;
+import org.springframework.data.repository.aot.generate.RepositoryContributor;
 import org.springframework.data.repository.config.AotRepositoryContext;
 import org.springframework.data.repository.config.RepositoryRegistrationAotProcessor;
 import org.springframework.data.util.TypeContributor;
@@ -31,7 +34,8 @@ public class AotMongoRepositoryPostProcessor extends RepositoryRegistrationAotPr
 	private final LazyLoadingProxyAotProcessor lazyLoadingProxyAotProcessor = new LazyLoadingProxyAotProcessor();
 
 	@Override
-	protected void contribute(AotRepositoryContext repositoryContext, GenerationContext generationContext) {
+	protected @Nullable RepositoryContributor contribute(AotRepositoryContext repositoryContext,
+			GenerationContext generationContext) {
 		// do some custom type registration here
 		super.contribute(repositoryContext, generationContext);
 
@@ -39,6 +43,14 @@ protected void contribute(AotRepositoryContext repositoryContext, GenerationCont
 			TypeContributor.contribute(type, it -> true, generationContext);
 			lazyLoadingProxyAotProcessor.registerLazyLoadingProxyIfNeeded(type, generationContext);
 		});
+
+		boolean enabled = Boolean.parseBoolean(
+				repositoryContext.getEnvironment().getProperty(AotContext.GENERATED_REPOSITORIES_ENABLED, "false"));
+		if (!enabled) {
+			return null;
+		}
+
+		return new MongoRepositoryContributor(repositoryContext);
 	}
 
 	@Override
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java
new file mode 100644
index 0000000000..17c19ad951
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java
@@ -0,0 +1,210 @@
+/*
+ * 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.mongodb.repository.aot;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import org.bson.conversions.Bson;
+import org.jspecify.annotations.NullUnmarked;
+import org.jspecify.annotations.Nullable;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Range;
+import org.springframework.data.domain.Score;
+import org.springframework.data.domain.ScrollPosition;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Vector;
+import org.springframework.data.geo.Distance;
+import org.springframework.data.geo.Point;
+import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
+import org.springframework.data.mongodb.core.convert.MongoWriter;
+import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
+import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
+import org.springframework.data.mongodb.core.query.Collation;
+import org.springframework.data.mongodb.core.query.CriteriaDefinition.Placeholder;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.mongodb.core.query.TextCriteria;
+import org.springframework.data.mongodb.core.query.UpdateDefinition;
+import org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor;
+import org.springframework.data.mongodb.repository.query.MongoParameterAccessor;
+import org.springframework.data.mongodb.repository.query.MongoQueryCreator;
+import org.springframework.data.repository.query.parser.PartTree;
+import org.springframework.data.util.TypeInformation;
+
+import com.mongodb.DBRef;
+
+/**
+ * @author Christoph Strobl
+ * @since 5.0
+ */
+class AotQueryCreator {
+
+	private MongoMappingContext mappingContext;
+
+	public AotQueryCreator() {
+
+		MongoMappingContext mongoMappingContext = new MongoMappingContext();
+		mongoMappingContext.setSimpleTypeHolder(
+				MongoCustomConversions.create((cfg) -> cfg.useNativeDriverJavaTimeCodecs()).getSimpleTypeHolder());
+		mongoMappingContext.setAutoIndexCreation(false);
+		mongoMappingContext.afterPropertiesSet();
+
+		this.mappingContext = mongoMappingContext;
+	}
+
+	@SuppressWarnings("NullAway")
+	StringQuery createQuery(PartTree partTree, int parameterCount) {
+
+		Query query = new MongoQueryCreator(partTree,
+				new PlaceholderConvertingParameterAccessor(new PlaceholderParameterAccessor(parameterCount)), mappingContext)
+				.createQuery();
+
+		if (partTree.isLimiting()) {
+			query.limit(partTree.getMaxResults());
+		}
+		return new StringQuery(query);
+	}
+
+	static class PlaceholderConvertingParameterAccessor extends ConvertingParameterAccessor {
+
+		/**
+		 * Creates a new {@link ConvertingParameterAccessor} with the given {@link MongoWriter} and delegate.
+		 *
+		 * @param delegate must not be {@literal null}.
+		 */
+		public PlaceholderConvertingParameterAccessor(PlaceholderParameterAccessor delegate) {
+			super(PlaceholderWriter.INSTANCE, delegate);
+		}
+	}
+
+	@NullUnmarked
+	enum PlaceholderWriter implements MongoWriter<Object> {
+
+		INSTANCE;
+
+		@Override
+		public @Nullable Object convertToMongoType(@Nullable Object obj, @Nullable TypeInformation<?> typeInformation) {
+			return obj instanceof Placeholder p ? p.getValue() : obj;
+		}
+
+		@Override
+		public DBRef toDBRef(Object object, @Nullable MongoPersistentProperty referringProperty) {
+			return null;
+		}
+
+		@Override
+		public void write(Object source, Bson sink) {
+
+		}
+	}
+
+	@NullUnmarked
+	static class PlaceholderParameterAccessor implements MongoParameterAccessor {
+
+		private final List<Placeholder> placeholders;
+
+		public PlaceholderParameterAccessor(int parameterCount) {
+			if (parameterCount == 0) {
+				placeholders = List.of();
+			} else {
+				placeholders = IntStream.range(0, parameterCount).mapToObj(Placeholder::indexed).collect(Collectors.toList());
+			}
+		}
+
+		@Override
+		public Range<Distance> getDistanceRange() {
+			return null;
+		}
+
+		@Override
+		public @Nullable Vector getVector() {
+			return null;
+		}
+
+		@Override
+		public @Nullable Score getScore() {
+			return null;
+		}
+
+		@Override
+		public @Nullable Range<Score> getScoreRange() {
+			return null;
+		}
+
+		@Override
+		public @Nullable Point getGeoNearLocation() {
+			return null;
+		}
+
+		@Override
+		public @Nullable TextCriteria getFullText() {
+			return null;
+		}
+
+		@Override
+		public @Nullable Collation getCollation() {
+			return null;
+		}
+
+		@Override
+		public Object[] getValues() {
+			return placeholders.toArray();
+		}
+
+		@Override
+		public @Nullable UpdateDefinition getUpdate() {
+			return null;
+		}
+
+		@Override
+		public @Nullable ScrollPosition getScrollPosition() {
+			return null;
+		}
+
+		@Override
+		public Pageable getPageable() {
+			return null;
+		}
+
+		@Override
+		public Sort getSort() {
+			return null;
+		}
+
+		@Override
+		public @Nullable Class<?> findDynamicProjection() {
+			return null;
+		}
+
+		@Override
+		public @Nullable Object getBindableValue(int index) {
+			return placeholders.get(index).getValue();
+		}
+
+		@Override
+		public boolean hasBindableNullValue() {
+			return false;
+		}
+
+		@Override
+		@SuppressWarnings({ "unchecked", "rawtypes" })
+		public Iterator<Object> iterator() {
+			return ((List) placeholders).iterator();
+		}
+	}
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java
new file mode 100644
index 0000000000..8e9439e7fa
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java
@@ -0,0 +1,154 @@
+/*
+ * 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.mongodb.repository.aot;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.bson.Document;
+import org.jspecify.annotations.Nullable;
+import org.springframework.data.mongodb.BindableMongoExpression;
+import org.springframework.data.mongodb.core.MongoOperations;
+import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
+import org.springframework.data.mongodb.core.aggregation.AggregationPipeline;
+import org.springframework.data.mongodb.core.convert.MongoConverter;
+import org.springframework.data.mongodb.core.mapping.FieldName;
+import org.springframework.data.mongodb.core.query.BasicQuery;
+import org.springframework.data.projection.ProjectionFactory;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.ObjectUtils;
+
+/**
+ * Support class for MongoDB AOT repository fragments.
+ *
+ * @author Christoph Strobl
+ * @since 5.0
+ */
+public class MongoAotRepositoryFragmentSupport {
+
+	private final RepositoryMetadata repositoryMetadata;
+	private final MongoOperations mongoOperations;
+	private final MongoConverter mongoConverter;
+	private final ProjectionFactory projectionFactory;
+
+	protected MongoAotRepositoryFragmentSupport(MongoOperations mongoOperations,
+			RepositoryFactoryBeanSupport.FragmentCreationContext context) {
+		this(mongoOperations, context.getRepositoryMetadata(), context.getProjectionFactory());
+	}
+
+	protected MongoAotRepositoryFragmentSupport(MongoOperations mongoOperations, RepositoryMetadata repositoryMetadata,
+			ProjectionFactory projectionFactory) {
+
+		this.mongoOperations = mongoOperations;
+		this.mongoConverter = mongoOperations.getConverter();
+		this.repositoryMetadata = repositoryMetadata;
+		this.projectionFactory = projectionFactory;
+	}
+
+	protected Document bindParameters(String source, Object[] parameters) {
+		return new BindableMongoExpression(source, this.mongoConverter, parameters).toDocument();
+	}
+
+	protected BasicQuery createQuery(String queryString, Object[] parameters) {
+
+		Document queryDocument = bindParameters(queryString, parameters);
+		return new BasicQuery(queryDocument);
+	}
+
+	protected AggregationPipeline createPipeline(List<Object> rawStages) {
+
+		List<AggregationOperation> stages = new ArrayList<>(rawStages.size());
+		boolean first = true;
+		for (Object rawStage : rawStages) {
+			if (rawStage instanceof Document stageDocument) {
+				if (first) {
+					stages.add((ctx) -> ctx.getMappedObject(stageDocument));
+				} else {
+					stages.add((ctx) -> stageDocument);
+				}
+			} else if (rawStage instanceof AggregationOperation aggregationOperation) {
+				stages.add(aggregationOperation);
+			} else {
+				throw new RuntimeException("%s cannot be converted to AggregationOperation".formatted(rawStage.getClass()));
+			}
+			if (first) {
+				first = false;
+			}
+		}
+		return new AggregationPipeline(stages);
+	}
+
+	protected List<Object> convertSimpleRawResults(Class<?> targetType, List<Document> rawResults) {
+
+		List<Object> list = new ArrayList<>(rawResults.size());
+		for (Document it : rawResults) {
+			list.add(extractSimpleTypeResult(it, targetType, mongoConverter));
+		}
+		return list;
+	}
+
+	protected Object convertSimpleRawResult(Class<?> targetType, Document rawResult) {
+		return extractSimpleTypeResult(rawResult, targetType, mongoConverter);
+	}
+
+	private static <T> @Nullable T extractSimpleTypeResult(@Nullable Document source, Class<T> targetType,
+			MongoConverter converter) {
+
+		if (ObjectUtils.isEmpty(source)) {
+			return null;
+		}
+
+		if (source.size() == 1) {
+			return getPotentiallyConvertedSimpleTypeValue(converter, source.values().iterator().next(), targetType);
+		}
+
+		Document intermediate = new Document(source);
+		intermediate.remove(FieldName.ID.name());
+
+		if (intermediate.size() == 1) {
+			return getPotentiallyConvertedSimpleTypeValue(converter, intermediate.values().iterator().next(), targetType);
+		}
+
+		for (Map.Entry<String, Object> entry : intermediate.entrySet()) {
+			if (entry != null && ClassUtils.isAssignable(targetType, entry.getValue().getClass())) {
+				return targetType.cast(entry.getValue());
+			}
+		}
+
+		throw new IllegalArgumentException(
+				String.format("o_O no entry of type %s found in %s.", targetType.getSimpleName(), source.toJson()));
+	}
+
+	@Nullable
+	@SuppressWarnings("unchecked")
+	private static <T> T getPotentiallyConvertedSimpleTypeValue(MongoConverter converter, @Nullable Object value,
+			Class<T> targetType) {
+
+		if (value == null) {
+			return null;
+		}
+
+		if (ClassUtils.isAssignableValue(targetType, value)) {
+			return (T) value;
+		}
+
+		return converter.getConversionService().convert(value, targetType);
+	}
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java
new file mode 100644
index 0000000000..999391f5ec
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java
@@ -0,0 +1,850 @@
+/*
+ * 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.mongodb.repository.aot;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+
+import org.bson.Document;
+import org.jspecify.annotations.NullUnmarked;
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.core.annotation.MergedAnnotation;
+import org.springframework.data.domain.SliceImpl;
+import org.springframework.data.domain.Sort.Order;
+import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery;
+import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove;
+import org.springframework.data.mongodb.core.ExecutableUpdateOperation.ExecutableUpdate;
+import org.springframework.data.mongodb.core.MongoOperations;
+import org.springframework.data.mongodb.core.aggregation.Aggregation;
+import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
+import org.springframework.data.mongodb.core.aggregation.AggregationPipeline;
+import org.springframework.data.mongodb.core.aggregation.AggregationResults;
+import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
+import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
+import org.springframework.data.mongodb.core.query.BasicQuery;
+import org.springframework.data.mongodb.core.query.BasicUpdate;
+import org.springframework.data.mongodb.core.query.Collation;
+import org.springframework.data.mongodb.repository.Hint;
+import org.springframework.data.mongodb.repository.Meta;
+import org.springframework.data.mongodb.repository.ReadPreference;
+import org.springframework.data.mongodb.repository.query.MongoQueryExecution.DeleteExecution;
+import org.springframework.data.mongodb.repository.query.MongoQueryExecution.PagedExecution;
+import org.springframework.data.mongodb.repository.query.MongoQueryExecution.SlicedExecution;
+import org.springframework.data.mongodb.repository.query.MongoQueryMethod;
+import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext;
+import org.springframework.data.util.ReflectionUtils;
+import org.springframework.javapoet.CodeBlock;
+import org.springframework.javapoet.CodeBlock.Builder;
+import org.springframework.javapoet.TypeName;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.NumberUtils;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * {@link CodeBlock} generator for common tasks.
+ *
+ * @author Christoph Strobl
+ * @since 5.0
+ */
+class MongoCodeBlocks {
+
+	private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)");
+
+	/**
+	 * Builder for generating query parsing {@link CodeBlock}.
+	 *
+	 * @param context
+	 * @param queryMethod
+	 * @return new instance of {@link QueryCodeBlockBuilder}.
+	 */
+	static QueryCodeBlockBuilder queryBlockBuilder(AotQueryMethodGenerationContext context,
+			MongoQueryMethod queryMethod) {
+		return new QueryCodeBlockBuilder(context, queryMethod);
+	}
+
+	/**
+	 * Builder for generating finder query execution {@link CodeBlock}.
+	 *
+	 * @param context
+	 * @param queryMethod
+	 * @return
+	 */
+	static QueryExecutionCodeBlockBuilder queryExecutionBlockBuilder(AotQueryMethodGenerationContext context,
+			MongoQueryMethod queryMethod) {
+
+		return new QueryExecutionCodeBlockBuilder(context, queryMethod);
+	}
+
+	/**
+	 * Builder for generating delete execution {@link CodeBlock}.
+	 *
+	 * @param context
+	 * @param queryMethod
+	 * @return
+	 */
+	static DeleteExecutionCodeBlockBuilder deleteExecutionBlockBuilder(AotQueryMethodGenerationContext context,
+			MongoQueryMethod queryMethod) {
+
+		return new DeleteExecutionCodeBlockBuilder(context, queryMethod);
+	}
+
+	/**
+	 * Builder for generating update parsing {@link CodeBlock}.
+	 *
+	 * @param context
+	 * @param queryMethod
+	 * @return
+	 */
+	static UpdateCodeBlockBuilder updateBlockBuilder(AotQueryMethodGenerationContext context,
+			MongoQueryMethod queryMethod) {
+		return new UpdateCodeBlockBuilder(context, queryMethod);
+	}
+
+	/**
+	 * Builder for generating update execution {@link CodeBlock}.
+	 *
+	 * @param context
+	 * @param queryMethod
+	 * @return
+	 */
+	static UpdateExecutionCodeBlockBuilder updateExecutionBlockBuilder(AotQueryMethodGenerationContext context,
+			MongoQueryMethod queryMethod) {
+
+		return new UpdateExecutionCodeBlockBuilder(context, queryMethod);
+	}
+
+	/**
+	 * Builder for generating aggregation (pipeline) parsing {@link CodeBlock}.
+	 *
+	 * @param context
+	 * @param queryMethod
+	 * @return
+	 */
+	static AggregationCodeBlockBuilder aggregationBlockBuilder(AotQueryMethodGenerationContext context,
+			MongoQueryMethod queryMethod) {
+
+		return new AggregationCodeBlockBuilder(context, queryMethod);
+	}
+
+	/**
+	 * Builder for generating aggregation execution {@link CodeBlock}.
+	 *
+	 * @param context
+	 * @param queryMethod
+	 * @return
+	 */
+	static AggregationExecutionCodeBlockBuilder aggregationExecutionBlockBuilder(AotQueryMethodGenerationContext context,
+			MongoQueryMethod queryMethod) {
+
+		return new AggregationExecutionCodeBlockBuilder(context, queryMethod);
+	}
+
+	@NullUnmarked
+	static class DeleteExecutionCodeBlockBuilder {
+
+		private final AotQueryMethodGenerationContext context;
+		private final MongoQueryMethod queryMethod;
+		private String queryVariableName;
+
+		DeleteExecutionCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
+
+			this.context = context;
+			this.queryMethod = queryMethod;
+		}
+
+		DeleteExecutionCodeBlockBuilder referencing(String queryVariableName) {
+
+			this.queryVariableName = queryVariableName;
+			return this;
+		}
+
+		CodeBlock build() {
+
+			String mongoOpsRef = context.fieldNameOf(MongoOperations.class);
+			Builder builder = CodeBlock.builder();
+
+			Class<?> domainType = context.getRepositoryInformation().getDomainType();
+			boolean isProjecting = context.getActualReturnType() != null
+					&& !ObjectUtils.nullSafeEquals(TypeName.get(domainType), context.getActualReturnType());
+
+			Object actualReturnType = isProjecting ? context.getActualReturnType().getType() : domainType;
+
+			builder.add("\n");
+			builder.addStatement("$T<$T> $L = $L.remove($T.class)", ExecutableRemove.class, domainType,
+					context.localVariable("remover"), mongoOpsRef, domainType);
+
+			DeleteExecution.Type type = DeleteExecution.Type.FIND_AND_REMOVE_ALL;
+			if (!queryMethod.isCollectionQuery()) {
+				if (!ClassUtils.isPrimitiveOrWrapper(context.getMethod().getReturnType())) {
+					type = DeleteExecution.Type.FIND_AND_REMOVE_ONE;
+				} else {
+					type = DeleteExecution.Type.ALL;
+				}
+			}
+
+			actualReturnType = ClassUtils.isPrimitiveOrWrapper(context.getMethod().getReturnType())
+					? TypeName.get(context.getMethod().getReturnType())
+					: queryMethod.isCollectionQuery() ? context.getReturnTypeName() : actualReturnType;
+
+			if (ClassUtils.isVoidType(context.getMethod().getReturnType())) {
+				builder.addStatement("new $T($L, $T.$L).execute($L)", DeleteExecution.class, context.localVariable("remover"),
+						DeleteExecution.Type.class, type.name(), queryVariableName);
+			} else if (context.getMethod().getReturnType() == Optional.class) {
+				builder.addStatement("return $T.ofNullable(($T) new $T($L, $T.$L).execute($L))", Optional.class,
+						actualReturnType, DeleteExecution.class, context.localVariable("remover"), DeleteExecution.Type.class,
+						type.name(), queryVariableName);
+			} else {
+				builder.addStatement("return ($T) new $T($L, $T.$L).execute($L)", actualReturnType, DeleteExecution.class,
+						context.localVariable("remover"), DeleteExecution.Type.class, type.name(), queryVariableName);
+			}
+
+			return builder.build();
+		}
+	}
+
+	@NullUnmarked
+	static class UpdateExecutionCodeBlockBuilder {
+
+		private final AotQueryMethodGenerationContext context;
+		private final MongoQueryMethod queryMethod;
+		private String queryVariableName;
+		private String updateVariableName;
+
+		UpdateExecutionCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
+
+			this.context = context;
+			this.queryMethod = queryMethod;
+		}
+
+		UpdateExecutionCodeBlockBuilder withFilter(String queryVariableName) {
+
+			this.queryVariableName = queryVariableName;
+			return this;
+		}
+
+		UpdateExecutionCodeBlockBuilder referencingUpdate(String updateVariableName) {
+
+			this.updateVariableName = updateVariableName;
+			return this;
+		}
+
+		CodeBlock build() {
+
+			String mongoOpsRef = context.fieldNameOf(MongoOperations.class);
+			Builder builder = CodeBlock.builder();
+
+			builder.add("\n");
+
+			String updateReference = updateVariableName;
+			Class<?> domainType = context.getRepositoryInformation().getDomainType();
+			builder.addStatement("$T<$T> $L = $L.update($T.class)", ExecutableUpdate.class, domainType,
+					context.localVariable("updater"), mongoOpsRef, domainType);
+
+			Class<?> returnType = ClassUtils.resolvePrimitiveIfNecessary(queryMethod.getReturnedObjectType());
+			if (ReflectionUtils.isVoid(returnType)) {
+				builder.addStatement("$L.matching($L).apply($L).all()", context.localVariable("updater"), queryVariableName,
+						updateReference);
+			} else if (ClassUtils.isAssignable(Long.class, returnType)) {
+				builder.addStatement("return $L.matching($L).apply($L).all().getModifiedCount()",
+						context.localVariable("updater"), queryVariableName, updateReference);
+			} else {
+				builder.addStatement("$T $L = $L.matching($L).apply($L).all().getModifiedCount()", Long.class,
+						context.localVariable("modifiedCount"), context.localVariable("updater"), queryVariableName,
+						updateReference);
+				builder.addStatement("return $T.convertNumberToTargetClass($L, $T.class)", NumberUtils.class,
+						context.localVariable("modifiedCount"), returnType);
+			}
+
+			return builder.build();
+		}
+	}
+
+	@NullUnmarked
+	static class AggregationExecutionCodeBlockBuilder {
+
+		private final AotQueryMethodGenerationContext context;
+		private final MongoQueryMethod queryMethod;
+		private String aggregationVariableName;
+
+		AggregationExecutionCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
+
+			this.context = context;
+			this.queryMethod = queryMethod;
+		}
+
+		AggregationExecutionCodeBlockBuilder referencing(String aggregationVariableName) {
+
+			this.aggregationVariableName = aggregationVariableName;
+			return this;
+		}
+
+		CodeBlock build() {
+
+			String mongoOpsRef = context.fieldNameOf(MongoOperations.class);
+			Builder builder = CodeBlock.builder();
+
+			builder.add("\n");
+
+			Class<?> outputType = queryMethod.getReturnedObjectType();
+			if (MongoSimpleTypes.HOLDER.isSimpleType(outputType)) {
+				outputType = Document.class;
+			} else if (ClassUtils.isAssignable(AggregationResults.class, outputType)) {
+				outputType = queryMethod.getReturnType().getComponentType().getType();
+			}
+
+			if (ReflectionUtils.isVoid(queryMethod.getReturnedObjectType())) {
+				builder.addStatement("$L.aggregate($L, $T.class)", mongoOpsRef, aggregationVariableName, outputType);
+				return builder.build();
+			}
+
+			if (ClassUtils.isAssignable(AggregationResults.class, context.getMethod().getReturnType())) {
+				builder.addStatement("return $L.aggregate($L, $T.class)", mongoOpsRef, aggregationVariableName, outputType);
+				return builder.build();
+			}
+
+			if (outputType == Document.class) {
+
+				Class<?> returnType = ClassUtils.resolvePrimitiveIfNecessary(queryMethod.getReturnedObjectType());
+
+				if (queryMethod.isStreamQuery()) {
+
+					builder.addStatement("$T<$T> $L = $L.aggregateStream($L, $T.class)", Stream.class, Document.class,
+							context.localVariable("results"), mongoOpsRef, aggregationVariableName, outputType);
+
+					builder.addStatement("return $L.map(it -> ($T) convertSimpleRawResult($T.class, it))",
+							context.localVariable("results"), returnType, returnType);
+				} else {
+
+					builder.addStatement("$T $L = $L.aggregate($L, $T.class)", AggregationResults.class,
+							context.localVariable("results"), mongoOpsRef, aggregationVariableName, outputType);
+
+					if (!queryMethod.isCollectionQuery()) {
+						builder.addStatement("return $T.<$T>firstElement(convertSimpleRawResults($T.class, $L.getMappedResults()))",
+								CollectionUtils.class, returnType, returnType, context.localVariable("results"));
+					} else {
+						builder.addStatement("return convertSimpleRawResults($T.class, $L.getMappedResults())", returnType,
+								context.localVariable("results"));
+					}
+				}
+			} else {
+				if (queryMethod.isSliceQuery()) {
+					builder.addStatement("$T $L = $L.aggregate($L, $T.class)", AggregationResults.class,
+							context.localVariable("results"), mongoOpsRef, aggregationVariableName, outputType);
+					builder.addStatement("boolean $L = $L.getMappedResults().size() > $L.getPageSize()",
+							context.localVariable("hasNext"), context.localVariable("results"), context.getPageableParameterName());
+					builder.addStatement(
+							"return new $T<>($L ? $L.getMappedResults().subList(0, $L.getPageSize()) : $L.getMappedResults(), $L, $L)",
+							SliceImpl.class, context.localVariable("hasNext"), context.localVariable("results"),
+							context.getPageableParameterName(), context.localVariable("results"), context.getPageableParameterName(),
+							context.localVariable("hasNext"));
+				} else {
+
+					if (queryMethod.isStreamQuery()) {
+						builder.addStatement("return $L.aggregateStream($L, $T.class)", mongoOpsRef, aggregationVariableName,
+								outputType);
+					} else {
+
+						builder.addStatement("return $L.aggregate($L, $T.class).getMappedResults()", mongoOpsRef,
+								aggregationVariableName, outputType);
+					}
+				}
+			}
+
+			return builder.build();
+		}
+	}
+
+	@NullUnmarked
+	static class QueryExecutionCodeBlockBuilder {
+
+		private final AotQueryMethodGenerationContext context;
+		private final MongoQueryMethod queryMethod;
+		private QueryInteraction query;
+
+		QueryExecutionCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
+
+			this.context = context;
+			this.queryMethod = queryMethod;
+		}
+
+		QueryExecutionCodeBlockBuilder forQuery(QueryInteraction query) {
+
+			this.query = query;
+			return this;
+		}
+
+		CodeBlock build() {
+
+			String mongoOpsRef = context.fieldNameOf(MongoOperations.class);
+
+			Builder builder = CodeBlock.builder();
+
+			boolean isProjecting = context.getReturnedType().isProjecting();
+			Class<?> domainType = context.getRepositoryInformation().getDomainType();
+			Object actualReturnType = queryMethod.getParameters().hasDynamicProjection() || isProjecting
+					? TypeName.get(context.getActualReturnType().getType())
+					: domainType;
+
+			builder.add("\n");
+
+			if (queryMethod.getParameters().hasDynamicProjection()) {
+				builder.addStatement("$T<$T> $L = $L.query($T.class).as($L)", FindWithQuery.class, actualReturnType,
+						context.localVariable("finder"), mongoOpsRef, domainType, context.getDynamicProjectionParameterName());
+			} else if (isProjecting) {
+				builder.addStatement("$T<$T> $L = $L.query($T.class).as($T.class)", FindWithQuery.class, actualReturnType,
+						context.localVariable("finder"), mongoOpsRef, domainType, actualReturnType);
+			} else {
+
+				builder.addStatement("$T<$T> $L = $L.query($T.class)", FindWithQuery.class, actualReturnType,
+						context.localVariable("finder"), mongoOpsRef, domainType);
+			}
+
+			String terminatingMethod;
+
+			if (queryMethod.isCollectionQuery() || queryMethod.isPageQuery() || queryMethod.isSliceQuery()) {
+				terminatingMethod = "all()";
+			} else if (query.isCount()) {
+				terminatingMethod = "count()";
+			} else if (query.isExists()) {
+				terminatingMethod = "exists()";
+			} else if (queryMethod.isStreamQuery()) {
+				terminatingMethod = "stream()";
+			} else {
+				terminatingMethod = Optional.class.isAssignableFrom(context.getReturnType().toClass()) ? "one()" : "oneValue()";
+			}
+
+			if (queryMethod.isPageQuery()) {
+				builder.addStatement("return new $T($L, $L).execute($L)", PagedExecution.class, context.localVariable("finder"),
+						context.getPageableParameterName(), query.name());
+			} else if (queryMethod.isSliceQuery()) {
+				builder.addStatement("return new $T($L, $L).execute($L)", SlicedExecution.class,
+						context.localVariable("finder"), context.getPageableParameterName(), query.name());
+			} else if (queryMethod.isScrollQuery()) {
+
+				String scrollPositionParameterName = context.getScrollPositionParameterName();
+
+				builder.addStatement("return $L.matching($L).scroll($L)", context.localVariable("finder"), query.name(),
+						scrollPositionParameterName);
+			} else {
+				if (query.isCount() && !ClassUtils.isAssignable(Long.class, context.getActualReturnType().getRawClass())) {
+
+					Class<?> returnType = ClassUtils.resolvePrimitiveIfNecessary(queryMethod.getReturnedObjectType());
+					builder.addStatement("return $T.convertNumberToTargetClass($L.matching($L).$L, $T.class)", NumberUtils.class,
+							context.localVariable("finder"), query.name(), terminatingMethod, returnType);
+
+				} else {
+					builder.addStatement("return $L.matching($L).$L", context.localVariable("finder"), query.name(),
+							terminatingMethod);
+				}
+			}
+
+			return builder.build();
+		}
+	}
+
+	@NullUnmarked
+	static class AggregationCodeBlockBuilder {
+
+		private final AotQueryMethodGenerationContext context;
+		private final MongoQueryMethod queryMethod;
+
+		private AggregationInteraction source;
+		private final List<String> arguments;
+		private String aggregationVariableName;
+		private boolean pipelineOnly;
+
+		AggregationCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
+
+			this.context = context;
+			this.arguments = context.getBindableParameterNames();
+			this.queryMethod = queryMethod;
+		}
+
+		AggregationCodeBlockBuilder stages(AggregationInteraction aggregation) {
+
+			this.source = aggregation;
+			return this;
+		}
+
+		AggregationCodeBlockBuilder usingAggregationVariableName(String aggregationVariableName) {
+
+			this.aggregationVariableName = aggregationVariableName;
+			return this;
+		}
+
+		AggregationCodeBlockBuilder pipelineOnly(boolean pipelineOnly) {
+
+			this.pipelineOnly = pipelineOnly;
+			return this;
+		}
+
+		CodeBlock build() {
+
+			CodeBlock.Builder builder = CodeBlock.builder();
+			builder.add("\n");
+
+			String pipelineName = context.localVariable(aggregationVariableName + (pipelineOnly ? "" : "Pipeline"));
+			builder.add(pipeline(pipelineName));
+
+			if (!pipelineOnly) {
+
+				builder.addStatement("$T<$T> $L = $T.newAggregation($T.class, $L.getOperations())", TypedAggregation.class,
+						context.getRepositoryInformation().getDomainType(), aggregationVariableName, Aggregation.class,
+						context.getRepositoryInformation().getDomainType(), pipelineName);
+
+				builder.add(aggregationOptions(aggregationVariableName));
+			}
+
+			return builder.build();
+		}
+
+		private CodeBlock pipeline(String pipelineVariableName) {
+
+			String sortParameter = context.getSortParameterName();
+			String limitParameter = context.getLimitParameterName();
+			String pageableParameter = context.getPageableParameterName();
+
+			boolean mightBeSorted = StringUtils.hasText(sortParameter);
+			boolean mightBeLimited = StringUtils.hasText(limitParameter);
+			boolean mightBePaged = StringUtils.hasText(pageableParameter);
+
+			int stageCount = source.stages().size();
+			if (mightBeSorted) {
+				stageCount++;
+			}
+			if (mightBeLimited) {
+				stageCount++;
+			}
+			if (mightBePaged) {
+				stageCount += 3;
+			}
+
+			Builder builder = CodeBlock.builder();
+			builder.add(aggregationStages(context.localVariable("stages"), source.stages(), stageCount, arguments));
+
+			if (mightBeSorted) {
+				builder.add(sortingStage(sortParameter));
+			}
+
+			if (mightBeLimited) {
+				builder.add(limitingStage(limitParameter));
+			}
+
+			if (mightBePaged) {
+				builder.add(pagingStage(pageableParameter, queryMethod.isSliceQuery()));
+			}
+
+			builder.addStatement("$T $L = createPipeline($L)", AggregationPipeline.class, pipelineVariableName,
+					context.localVariable("stages"));
+			return builder.build();
+		}
+
+		private CodeBlock aggregationOptions(String aggregationVariableName) {
+
+			Builder builder = CodeBlock.builder();
+			List<CodeBlock> options = new ArrayList<>(5);
+			if (ReflectionUtils.isVoid(queryMethod.getReturnedObjectType())) {
+				options.add(CodeBlock.of(".skipOutput()"));
+			}
+
+			MergedAnnotation<Hint> hintAnnotation = context.getAnnotation(Hint.class);
+			String hint = hintAnnotation.isPresent() ? hintAnnotation.getString("value") : null;
+			if (StringUtils.hasText(hint)) {
+				options.add(CodeBlock.of(".hint($S)", hint));
+			}
+
+			MergedAnnotation<ReadPreference> readPreferenceAnnotation = context.getAnnotation(ReadPreference.class);
+			String readPreference = readPreferenceAnnotation.isPresent() ? readPreferenceAnnotation.getString("value") : null;
+			if (StringUtils.hasText(readPreference)) {
+				options.add(CodeBlock.of(".readPreference($T.valueOf($S))", com.mongodb.ReadPreference.class, readPreference));
+			}
+
+			if (queryMethod.hasAnnotatedCollation()) {
+				options.add(CodeBlock.of(".collation($T.parse($S))", Collation.class, queryMethod.getAnnotatedCollation()));
+			}
+
+			if (!options.isEmpty()) {
+
+				Builder optionsBuilder = CodeBlock.builder();
+				optionsBuilder.add("$T $L = $T.builder()\n", AggregationOptions.class,
+						context.localVariable("aggregationOptions"), AggregationOptions.class);
+				optionsBuilder.indent();
+				for (CodeBlock optionBlock : options) {
+					optionsBuilder.add(optionBlock);
+					optionsBuilder.add("\n");
+				}
+				optionsBuilder.add(".build();\n");
+				optionsBuilder.unindent();
+				builder.add(optionsBuilder.build());
+
+				builder.addStatement("$L = $L.withOptions($L)", aggregationVariableName, aggregationVariableName,
+						context.localVariable("aggregationOptions"));
+			}
+			return builder.build();
+		}
+
+		private CodeBlock aggregationStages(String stageListVariableName, Iterable<String> stages, int stageCount,
+				List<String> arguments) {
+
+			Builder builder = CodeBlock.builder();
+			builder.addStatement("$T<$T> $L = new $T($L)", List.class, Object.class, stageListVariableName, ArrayList.class,
+					stageCount);
+			int stageCounter = 0;
+
+			for (String stage : stages) {
+				String stageName = context.localVariable("stage_%s".formatted(stageCounter++));
+				builder.add(renderExpressionToDocument(stage, stageName, arguments));
+				builder.addStatement("$L.add($L)", context.localVariable("stages"), stageName);
+			}
+
+			return builder.build();
+		}
+
+		private CodeBlock sortingStage(String sortProvider) {
+
+			Builder builder = CodeBlock.builder();
+
+			builder.beginControlFlow("if ($L.isSorted())", sortProvider);
+			builder.addStatement("$T $L = new $T()", Document.class, context.localVariable("sortDocument"), Document.class);
+			builder.beginControlFlow("for ($T $L : $L)", Order.class, context.localVariable("order"), sortProvider);
+			builder.addStatement("$L.append($L.getProperty(), $L.isAscending() ? 1 : -1);",
+					context.localVariable("sortDocument"), context.localVariable("order"), context.localVariable("order"));
+			builder.endControlFlow();
+			builder.addStatement("stages.add(new $T($S, $L))", Document.class, "$sort",
+					context.localVariable("sortDocument"));
+			builder.endControlFlow();
+
+			return builder.build();
+		}
+
+		private CodeBlock pagingStage(String pageableProvider, boolean slice) {
+
+			Builder builder = CodeBlock.builder();
+
+			builder.add(sortingStage(pageableProvider + ".getSort()"));
+
+			builder.beginControlFlow("if ($L.isPaged())", pageableProvider);
+			builder.beginControlFlow("if ($L.getOffset() > 0)", pageableProvider);
+			builder.addStatement("$L.add($T.skip($L.getOffset()))", context.localVariable("stages"), Aggregation.class,
+					pageableProvider);
+			builder.endControlFlow();
+			if (slice) {
+				builder.addStatement("$L.add($T.limit($L.getPageSize() + 1))", context.localVariable("stages"),
+						Aggregation.class, pageableProvider);
+			} else {
+				builder.addStatement("$L.add($T.limit($L.getPageSize()))", context.localVariable("stages"), Aggregation.class,
+						pageableProvider);
+			}
+			builder.endControlFlow();
+
+			return builder.build();
+		}
+
+		private CodeBlock limitingStage(String limitProvider) {
+
+			Builder builder = CodeBlock.builder();
+
+			builder.beginControlFlow("if ($L.isLimited())", limitProvider);
+			builder.addStatement("$L.add($T.limit($L.max()))", context.localVariable("stages"), Aggregation.class,
+					limitProvider);
+			builder.endControlFlow();
+
+			return builder.build();
+		}
+
+	}
+
+	@NullUnmarked
+	static class QueryCodeBlockBuilder {
+
+		private final AotQueryMethodGenerationContext context;
+		private final MongoQueryMethod queryMethod;
+
+		private QueryInteraction source;
+		private final List<String> arguments;
+		private String queryVariableName;
+
+		QueryCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
+
+			this.context = context;
+			this.arguments = context.getBindableParameterNames();
+			this.queryMethod = queryMethod;
+		}
+
+		QueryCodeBlockBuilder filter(QueryInteraction query) {
+
+			this.source = query;
+			return this;
+		}
+
+		QueryCodeBlockBuilder usingQueryVariableName(String queryVariableName) {
+			this.queryVariableName = queryVariableName;
+			return this;
+		}
+
+		CodeBlock build() {
+
+			CodeBlock.Builder builder = CodeBlock.builder();
+
+			builder.add("\n");
+			builder.add(renderExpressionToQuery(source.getQuery().getQueryString(), queryVariableName));
+
+			if (StringUtils.hasText(source.getQuery().getFieldsString())) {
+
+				builder.add(renderExpressionToDocument(source.getQuery().getFieldsString(), "fields", arguments));
+				builder.addStatement("$L.setFieldsObject(fields)", queryVariableName);
+			}
+
+			String sortParameter = context.getSortParameterName();
+			if (StringUtils.hasText(sortParameter)) {
+				builder.addStatement("$L.with($L)", queryVariableName, sortParameter);
+			} else if (StringUtils.hasText(source.getQuery().getSortString())) {
+
+				builder.add(renderExpressionToDocument(source.getQuery().getSortString(), "sort", arguments));
+				builder.addStatement("$L.setSortObject(sort)", queryVariableName);
+			}
+
+			String limitParameter = context.getLimitParameterName();
+			if (StringUtils.hasText(limitParameter)) {
+				builder.addStatement("$L.limit($L)", queryVariableName, limitParameter);
+			} else if (context.getPageableParameterName() == null && source.getQuery().isLimited()) {
+				builder.addStatement("$L.limit($L)", queryVariableName, source.getQuery().getLimit());
+			}
+
+			String pageableParameter = context.getPageableParameterName();
+			if (StringUtils.hasText(pageableParameter) && !queryMethod.isPageQuery() && !queryMethod.isSliceQuery()) {
+				builder.addStatement("$L.with($L)", queryVariableName, pageableParameter);
+			}
+
+			MergedAnnotation<Hint> hintAnnotation = context.getAnnotation(Hint.class);
+			String hint = hintAnnotation.isPresent() ? hintAnnotation.getString("value") : null;
+
+			if (StringUtils.hasText(hint)) {
+				builder.addStatement("$L.withHint($S)", queryVariableName, hint);
+			}
+
+			MergedAnnotation<ReadPreference> readPreferenceAnnotation = context.getAnnotation(ReadPreference.class);
+			String readPreference = readPreferenceAnnotation.isPresent() ? readPreferenceAnnotation.getString("value") : null;
+
+			if (StringUtils.hasText(readPreference)) {
+				builder.addStatement("$L.withReadPreference($T.valueOf($S))", queryVariableName,
+						com.mongodb.ReadPreference.class, readPreference);
+			}
+
+			MergedAnnotation<Meta> metaAnnotation = context.getAnnotation(Meta.class);
+
+			if (metaAnnotation.isPresent()) {
+
+				long maxExecutionTimeMs = metaAnnotation.getLong("maxExecutionTimeMs");
+				if (maxExecutionTimeMs != -1) {
+					builder.addStatement("$L.maxTimeMsec($L)", queryVariableName, maxExecutionTimeMs);
+				}
+
+				int cursorBatchSize = metaAnnotation.getInt("cursorBatchSize");
+				if (cursorBatchSize != 0) {
+					builder.addStatement("$L.cursorBatchSize($L)", queryVariableName, cursorBatchSize);
+				}
+
+				String comment = metaAnnotation.getString("comment");
+				if (StringUtils.hasText("comment")) {
+					builder.addStatement("$L.comment($S)", queryVariableName, comment);
+				}
+			}
+
+			// TODO: Meta annotation: Disk usage
+
+			return builder.build();
+		}
+
+		private CodeBlock renderExpressionToQuery(@Nullable String source, String variableName) {
+
+			Builder builder = CodeBlock.builder();
+			if (!StringUtils.hasText(source)) {
+
+				builder.addStatement("$T $L = new $T(new $T())", BasicQuery.class, variableName, BasicQuery.class,
+						Document.class);
+			} else if (!containsPlaceholder(source)) {
+				builder.addStatement("$T $L = new $T($T.parse($S))", BasicQuery.class, variableName, BasicQuery.class,
+						Document.class, source);
+			} else {
+				builder.addStatement("$T $L = createQuery($S, new $T[]{ $L })", BasicQuery.class, variableName, source,
+						Object.class, StringUtils.collectionToDelimitedString(arguments, ", "));
+			}
+
+			return builder.build();
+		}
+	}
+
+	@NullUnmarked
+	static class UpdateCodeBlockBuilder {
+
+		private UpdateInteraction source;
+		private List<String> arguments;
+		private String updateVariableName;
+
+		public UpdateCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
+			this.arguments = context.getBindableParameterNames();
+		}
+
+		public UpdateCodeBlockBuilder update(UpdateInteraction update) {
+			this.source = update;
+			return this;
+		}
+
+		public UpdateCodeBlockBuilder usingUpdateVariableName(String updateVariableName) {
+			this.updateVariableName = updateVariableName;
+			return this;
+		}
+
+		CodeBlock build() {
+
+			CodeBlock.Builder builder = CodeBlock.builder();
+
+			builder.add("\n");
+			String tmpVariableName = updateVariableName + "Document";
+			builder.add(renderExpressionToDocument(source.getUpdate().getUpdateString(), tmpVariableName, arguments));
+			builder.addStatement("$T $L = new $T($L)", BasicUpdate.class, updateVariableName, BasicUpdate.class,
+					tmpVariableName);
+
+			return builder.build();
+		}
+	}
+
+	private static CodeBlock renderExpressionToDocument(@Nullable String source, String variableName,
+			List<String> arguments) {
+
+		Builder builder = CodeBlock.builder();
+		if (!StringUtils.hasText(source)) {
+			builder.addStatement("$T $L = new $T()", Document.class, variableName, Document.class);
+		} else if (!containsPlaceholder(source)) {
+			builder.addStatement("$T $L = $T.parse($S)", Document.class, variableName, Document.class, source);
+		} else {
+			builder.addStatement("$T $L = bindParameters($S, new $T[]{ $L })", Document.class, variableName, source,
+					Object.class, StringUtils.collectionToDelimitedString(arguments, ", "));
+		}
+		return builder.build();
+	}
+
+	private static boolean containsPlaceholder(String source) {
+		return PARAMETER_BINDING_PATTERN.matcher(source).find();
+	}
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoInteraction.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoInteraction.java
new file mode 100644
index 0000000000..fa9ca2f99e
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoInteraction.java
@@ -0,0 +1,62 @@
+/*
+ * 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.mongodb.repository.aot;
+
+/**
+ * Base abstraction for interactions with MongoDB.
+ *
+ * @author Christoph Strobl
+ * @since 5.0
+ */
+abstract class MongoInteraction {
+
+	abstract InteractionType getExecutionType();
+
+	boolean isAggregation() {
+		return InteractionType.AGGREGATION.equals(getExecutionType());
+	}
+
+	boolean isCount() {
+		return InteractionType.COUNT.equals(getExecutionType());
+	}
+
+	boolean isDelete() {
+		return InteractionType.DELETE.equals(getExecutionType());
+	}
+
+	boolean isExists() {
+		return InteractionType.EXISTS.equals(getExecutionType());
+	}
+
+	boolean isUpdate() {
+		return InteractionType.UPDATE.equals(getExecutionType());
+	}
+
+	String name() {
+
+		if (isDelete()) {
+			return "deleteQuery";
+		}
+		if (isCount()) {
+			return "countQuery";
+		}
+		return "filterQuery";
+	}
+
+	enum InteractionType {
+		QUERY, COUNT, DELETE, EXISTS, UPDATE, AGGREGATION
+	}
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java
new file mode 100644
index 0000000000..424d067d74
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java
@@ -0,0 +1,283 @@
+/*
+ * 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.mongodb.repository.aot;
+
+import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.*;
+
+import java.lang.reflect.Method;
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.jspecify.annotations.Nullable;
+import org.springframework.core.annotation.AnnotatedElementUtils;
+import org.springframework.data.mongodb.core.MongoOperations;
+import org.springframework.data.mongodb.core.aggregation.AggregationUpdate;
+import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
+import org.springframework.data.mongodb.repository.Query;
+import org.springframework.data.mongodb.repository.Update;
+import org.springframework.data.mongodb.repository.query.MongoQueryMethod;
+import org.springframework.data.repository.aot.generate.AotRepositoryClassBuilder;
+import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder;
+import org.springframework.data.repository.aot.generate.MethodContributor;
+import org.springframework.data.repository.aot.generate.RepositoryContributor;
+import org.springframework.data.repository.config.AotRepositoryContext;
+import org.springframework.data.repository.core.RepositoryInformation;
+import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
+import org.springframework.data.repository.query.QueryMethod;
+import org.springframework.data.repository.query.parser.PartTree;
+import org.springframework.javapoet.CodeBlock;
+import org.springframework.javapoet.TypeName;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * MongoDB specific {@link RepositoryContributor}.
+ *
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ * @since 5.0
+ */
+public class MongoRepositoryContributor extends RepositoryContributor {
+
+	private static final Log logger = LogFactory.getLog(MongoRepositoryContributor.class);
+
+	private final AotQueryCreator queryCreator;
+	private final MongoMappingContext mappingContext;
+
+	public MongoRepositoryContributor(AotRepositoryContext repositoryContext) {
+
+		super(repositoryContext);
+		this.queryCreator = new AotQueryCreator();
+		this.mappingContext = new MongoMappingContext();
+	}
+
+	@Override
+	protected void customizeClass(AotRepositoryClassBuilder classBuilder) {
+		classBuilder.customize(builder -> builder.superclass(TypeName.get(MongoAotRepositoryFragmentSupport.class)));
+	}
+
+	@Override
+	protected void customizeConstructor(AotRepositoryConstructorBuilder constructorBuilder) {
+
+		constructorBuilder.addParameter("operations", TypeName.get(MongoOperations.class));
+		constructorBuilder.addParameter("context", TypeName.get(RepositoryFactoryBeanSupport.FragmentCreationContext.class),
+				false);
+
+		constructorBuilder.customize((builder) -> {
+			builder.addStatement("super(operations, context)");
+		});
+	}
+
+	@Override
+	@SuppressWarnings("NullAway")
+	protected @Nullable MethodContributor<? extends QueryMethod> contributeQueryMethod(Method method) {
+
+		MongoQueryMethod queryMethod = new MongoQueryMethod(method, getRepositoryInformation(), getProjectionFactory(),
+				mappingContext);
+
+		if (queryMethod.hasAnnotatedAggregation()) {
+			AggregationInteraction aggregation = new AggregationInteraction(queryMethod.getAnnotatedAggregation());
+			return aggregationMethodContributor(queryMethod, aggregation);
+		}
+
+		QueryInteraction query = createStringQuery(getRepositoryInformation(), queryMethod,
+				AnnotatedElementUtils.findMergedAnnotation(method, Query.class), method.getParameterCount());
+
+		if (queryMethod.hasAnnotatedQuery()) {
+			if (StringUtils.hasText(queryMethod.getAnnotatedQuery())
+					&& Pattern.compile("[\\?:][#$]\\{.*\\}").matcher(queryMethod.getAnnotatedQuery()).find()) {
+
+				if (logger.isDebugEnabled()) {
+					logger.debug(
+							"Skipping AOT generation for [%s]. SpEL expressions are not supported".formatted(method.getName()));
+				}
+				return MethodContributor.forQueryMethod(queryMethod).metadataOnly(query);
+			}
+		}
+
+		if (backoff(queryMethod)) {
+			return null;
+		}
+
+		if (query.isDelete()) {
+			return deleteMethodContributor(queryMethod, query);
+		}
+
+		if (queryMethod.isModifyingQuery()) {
+
+			int updateIndex = queryMethod.getParameters().getUpdateIndex();
+			if (updateIndex != -1) {
+
+				UpdateInteraction update = new UpdateInteraction(query, null, updateIndex);
+				return updateMethodContributor(queryMethod, update);
+
+			} else {
+				Update updateSource = queryMethod.getUpdateSource();
+				if (StringUtils.hasText(updateSource.value())) {
+					UpdateInteraction update = new UpdateInteraction(query, new StringUpdate(updateSource.value()), null);
+					return updateMethodContributor(queryMethod, update);
+				}
+
+				if (!ObjectUtils.isEmpty(updateSource.pipeline())) {
+					AggregationUpdateInteraction update = new AggregationUpdateInteraction(query, updateSource.pipeline());
+					return aggregationUpdateMethodContributor(queryMethod, update);
+				}
+			}
+		}
+
+		return queryMethodContributor(queryMethod, query);
+	}
+
+	@SuppressWarnings("NullAway")
+	private QueryInteraction createStringQuery(RepositoryInformation repositoryInformation, MongoQueryMethod queryMethod,
+			@Nullable Query queryAnnotation, int parameterCount) {
+
+		QueryInteraction query;
+		if (queryMethod.hasAnnotatedQuery() && queryAnnotation != null) {
+			query = new QueryInteraction(new StringQuery(queryMethod.getAnnotatedQuery()), queryAnnotation.count(),
+					queryAnnotation.delete(), queryAnnotation.exists());
+		} else {
+
+			PartTree partTree = new PartTree(queryMethod.getName(), repositoryInformation.getDomainType());
+			query = new QueryInteraction(queryCreator.createQuery(partTree, parameterCount), partTree.isCountProjection(),
+					partTree.isDelete(), partTree.isExistsProjection());
+		}
+
+		if (queryAnnotation != null && StringUtils.hasText(queryAnnotation.sort())) {
+			query = query.withSort(queryAnnotation.sort());
+		}
+		if (queryAnnotation != null && StringUtils.hasText(queryAnnotation.fields())) {
+			query = query.withFields(queryAnnotation.fields());
+		}
+
+		return query;
+	}
+
+	private static boolean backoff(MongoQueryMethod method) {
+
+		// TODO: namedQuery, Regex queries, queries accepting Shapes (e.g. within) or returning arrays.
+		boolean skip = method.isGeoNearQuery() || method.isSearchQuery()
+				|| method.getName().toLowerCase(Locale.ROOT).contains("regex") || method.getReturnType().getType().isArray();
+
+		if (skip && logger.isDebugEnabled()) {
+			logger.debug("Skipping AOT generation for [%s]. Method is either returning an array or a geo-near, regex query"
+					.formatted(method.getName()));
+		}
+		return skip;
+	}
+
+	private static MethodContributor<MongoQueryMethod> aggregationMethodContributor(MongoQueryMethod queryMethod,
+			AggregationInteraction aggregation) {
+
+		return MethodContributor.forQueryMethod(queryMethod).withMetadata(aggregation).contribute(context -> {
+
+			CodeBlock.Builder builder = CodeBlock.builder();
+
+			builder.add(aggregationBlockBuilder(context, queryMethod).stages(aggregation)
+					.usingAggregationVariableName("aggregation").build());
+			builder.add(aggregationExecutionBlockBuilder(context, queryMethod).referencing("aggregation").build());
+
+			return builder.build();
+		});
+	}
+
+	private static MethodContributor<MongoQueryMethod> updateMethodContributor(MongoQueryMethod queryMethod,
+			UpdateInteraction update) {
+
+		return MethodContributor.forQueryMethod(queryMethod).withMetadata(update).contribute(context -> {
+
+			CodeBlock.Builder builder = CodeBlock.builder();
+
+			// update filter
+			String filterVariableName = context.localVariable(update.name());
+			builder.add(queryBlockBuilder(context, queryMethod).filter(update.getFilter())
+					.usingQueryVariableName(filterVariableName).build());
+
+			// update definition
+			String updateVariableName;
+
+			if (update.hasUpdateDefinitionParameter()) {
+				updateVariableName = context.getParameterName(update.getRequiredUpdateDefinitionParameter());
+			} else {
+				updateVariableName = context.localVariable("updateDefinition");
+				builder.add(updateBlockBuilder(context, queryMethod).update(update).usingUpdateVariableName(updateVariableName)
+						.build());
+			}
+
+			builder.add(updateExecutionBlockBuilder(context, queryMethod).withFilter(filterVariableName)
+					.referencingUpdate(updateVariableName).build());
+			return builder.build();
+		});
+	}
+
+	private static MethodContributor<MongoQueryMethod> aggregationUpdateMethodContributor(MongoQueryMethod queryMethod,
+			AggregationUpdateInteraction update) {
+
+		return MethodContributor.forQueryMethod(queryMethod).withMetadata(update).contribute(context -> {
+
+			CodeBlock.Builder builder = CodeBlock.builder();
+
+			// update filter
+			String filterVariableName = context.localVariable(update.name());
+			QueryCodeBlockBuilder queryCodeBlockBuilder = queryBlockBuilder(context, queryMethod).filter(update.getFilter());
+			builder.add(queryCodeBlockBuilder.usingQueryVariableName(filterVariableName).build());
+
+			// update definition
+			String updateVariableName = "updateDefinition";
+			builder.add(aggregationBlockBuilder(context, queryMethod).stages(update)
+					.usingAggregationVariableName(updateVariableName).pipelineOnly(true).build());
+
+			builder.addStatement("$T $L = $T.from($L.getOperations())", AggregationUpdate.class,
+					context.localVariable("aggregationUpdate"), AggregationUpdate.class, updateVariableName);
+
+			builder.add(updateExecutionBlockBuilder(context, queryMethod).withFilter(filterVariableName)
+					.referencingUpdate(context.localVariable("aggregationUpdate")).build());
+			return builder.build();
+		});
+	}
+
+	private static MethodContributor<MongoQueryMethod> deleteMethodContributor(MongoQueryMethod queryMethod,
+			QueryInteraction query) {
+
+		return MethodContributor.forQueryMethod(queryMethod).withMetadata(query).contribute(context -> {
+
+			CodeBlock.Builder builder = CodeBlock.builder();
+			QueryCodeBlockBuilder queryCodeBlockBuilder = queryBlockBuilder(context, queryMethod).filter(query);
+
+			String queryVariableName = context.localVariable(query.name());
+			builder.add(queryCodeBlockBuilder.usingQueryVariableName(queryVariableName).build());
+			builder.add(deleteExecutionBlockBuilder(context, queryMethod).referencing(queryVariableName).build());
+			return builder.build();
+		});
+	}
+
+	private static MethodContributor<MongoQueryMethod> queryMethodContributor(MongoQueryMethod queryMethod,
+			QueryInteraction query) {
+
+		return MethodContributor.forQueryMethod(queryMethod).withMetadata(query).contribute(context -> {
+
+			CodeBlock.Builder builder = CodeBlock.builder();
+			QueryCodeBlockBuilder queryCodeBlockBuilder = queryBlockBuilder(context, queryMethod).filter(query);
+
+			builder.add(queryCodeBlockBuilder.usingQueryVariableName(context.localVariable(query.name())).build());
+			builder.add(queryExecutionBlockBuilder(context, queryMethod).forQuery(query).build());
+			return builder.build();
+		});
+	}
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryInteraction.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryInteraction.java
new file mode 100644
index 0000000000..563079c03b
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryInteraction.java
@@ -0,0 +1,83 @@
+/*
+ * 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.mongodb.repository.aot;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.springframework.data.repository.aot.generate.QueryMetadata;
+import org.springframework.util.StringUtils;
+
+/**
+ * An {@link MongoInteraction} to execute a query.
+ *
+ * @author Christoph Strobl
+ * @since 5.0
+ */
+class QueryInteraction extends MongoInteraction implements QueryMetadata {
+
+	private final StringQuery query;
+	private final InteractionType interactionType;
+
+	QueryInteraction(StringQuery query, boolean count, boolean delete, boolean exists) {
+
+		this.query = query;
+		if (count) {
+			interactionType = InteractionType.COUNT;
+		} else if (exists) {
+			interactionType = InteractionType.EXISTS;
+		} else if (delete) {
+			interactionType = InteractionType.DELETE;
+		} else {
+			interactionType = InteractionType.QUERY;
+		}
+	}
+
+	StringQuery getQuery() {
+		return query;
+	}
+
+	QueryInteraction withSort(String sort) {
+		query.sort(sort);
+		return this;
+	}
+
+	QueryInteraction withFields(String fields) {
+		query.fields(fields);
+		return this;
+	}
+
+	@Override
+	InteractionType getExecutionType() {
+		return interactionType;
+	}
+
+	@Override
+	public Map<String, Object> serialize() {
+
+		Map<String, Object> serialized = new LinkedHashMap<>();
+
+		serialized.put("filter", query.getQueryString());
+		if (query.isSorted()) {
+			serialized.put("sort", query.getSortString());
+		}
+		if (StringUtils.hasText(query.getFieldsString())) {
+			serialized.put("fields", query.getFieldsString());
+		}
+
+		return serialized;
+	}
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/RepositoryRuntimeHints.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/RepositoryRuntimeHints.java
index b1ba6ea3f0..00ff498731 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/RepositoryRuntimeHints.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/RepositoryRuntimeHints.java
@@ -19,6 +19,7 @@
 
 import java.util.List;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.aot.hint.MemberCategory;
 import org.springframework.aot.hint.RuntimeHints;
 import org.springframework.aot.hint.RuntimeHintsRegistrar;
@@ -28,7 +29,6 @@
 import org.springframework.data.mongodb.repository.support.QuerydslMongoPredicateExecutor;
 import org.springframework.data.mongodb.repository.support.ReactiveQuerydslMongoPredicateExecutor;
 import org.springframework.data.querydsl.QuerydslUtils;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ClassUtils;
 
 /**
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringAggregation.java
new file mode 100644
index 0000000000..7b73215e98
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringAggregation.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 org.springframework.data.mongodb.repository.aot;
+
+/**
+ * Value object holding the raw representation of an Aggregation Pipeline.
+ * 
+ * @author Christoph Strobl
+ * @since 5.0
+ */
+record StringAggregation(String[] pipeline) {
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringQuery.java
new file mode 100644
index 0000000000..d037198bba
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringQuery.java
@@ -0,0 +1,198 @@
+/*
+ * 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.mongodb.repository.aot;
+
+import java.util.Optional;
+import java.util.Set;
+
+import org.bson.Document;
+import org.jspecify.annotations.Nullable;
+import org.springframework.data.domain.KeysetScrollPosition;
+import org.springframework.data.mongodb.core.query.Collation;
+import org.springframework.data.mongodb.core.query.Field;
+import org.springframework.data.mongodb.core.query.Meta;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.mongodb.util.BsonUtils;
+import org.springframework.util.StringUtils;
+
+import com.mongodb.ReadConcern;
+import com.mongodb.ReadPreference;
+
+/**
+ * Helper to capture setting for AOT queries.
+ *
+ * @author Christoph Strobl
+ * @since 5.0
+ */
+class StringQuery extends Query {
+
+	private Query delegate;
+	private @Nullable String raw;
+	private @Nullable String sort;
+	private @Nullable String fields;
+
+	public StringQuery(Query query) {
+		this.delegate = query;
+	}
+
+	public StringQuery(String query) {
+		this.delegate = new Query();
+		this.raw = query;
+	}
+
+	@Nullable
+	String getQueryString() {
+
+		if (StringUtils.hasText(raw)) {
+			return raw;
+		}
+
+		Document queryObj = getQueryObject();
+		if (queryObj.isEmpty()) {
+			return null;
+		}
+		return toJson(queryObj);
+	}
+
+	public Query sort(String sort) {
+		this.sort = sort;
+		return this;
+	}
+
+	@Override
+	public Field fields() {
+		return delegate.fields();
+	}
+
+	@Override
+	public boolean hasReadConcern() {
+		return delegate.hasReadConcern();
+	}
+
+	@Override
+	public @Nullable ReadConcern getReadConcern() {
+		return delegate.getReadConcern();
+	}
+
+	@Override
+	public boolean hasReadPreference() {
+		return delegate.hasReadPreference();
+	}
+
+	@Override
+	public @Nullable ReadPreference getReadPreference() {
+		return delegate.getReadPreference();
+	}
+
+	@Override
+	public boolean hasKeyset() {
+		return delegate.hasKeyset();
+	}
+
+	@Override
+	public @Nullable KeysetScrollPosition getKeyset() {
+		return delegate.getKeyset();
+	}
+
+	@Override
+	public Set<Class<?>> getRestrictedTypes() {
+		return delegate.getRestrictedTypes();
+	}
+
+	@Override
+	public Document getQueryObject() {
+		return delegate.getQueryObject();
+	}
+
+	@Override
+	public Document getFieldsObject() {
+		return delegate.getFieldsObject();
+	}
+
+	@Override
+	public Document getSortObject() {
+		return delegate.getSortObject();
+	}
+
+	@Override
+	public boolean isSorted() {
+		return delegate.isSorted() || StringUtils.hasText(sort);
+	}
+
+	@Override
+	public long getSkip() {
+		return delegate.getSkip();
+	}
+
+	@Override
+	public boolean isLimited() {
+		return delegate.isLimited();
+	}
+
+	@Override
+	public int getLimit() {
+		return delegate.getLimit();
+	}
+
+	@Override
+	public @Nullable String getHint() {
+		return delegate.getHint();
+	}
+
+	@Override
+	public Meta getMeta() {
+		return delegate.getMeta();
+	}
+
+	@Override
+	public Optional<Collation> getCollation() {
+		return delegate.getCollation();
+	}
+
+	@Nullable
+	String getSortString() {
+		if (StringUtils.hasText(sort)) {
+			return sort;
+		}
+		Document sort = getSortObject();
+		if (sort.isEmpty()) {
+			return null;
+		}
+		return toJson(sort);
+	}
+
+	@Nullable
+	String getFieldsString() {
+		if (StringUtils.hasText(fields)) {
+			return fields;
+		}
+
+		Document fields = getFieldsObject();
+		if (fields.isEmpty()) {
+			return null;
+		}
+		return toJson(fields);
+	}
+
+	StringQuery fields(String fields) {
+		this.fields = fields;
+		return this;
+	}
+
+	String toJson(Document source) {
+		return BsonUtils.writeJson(source).toJsonString();
+	}
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringUpdate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringUpdate.java
new file mode 100644
index 0000000000..f65ee7912f
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringUpdate.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 org.springframework.data.mongodb.repository.aot;
+
+/**
+ * @author Christoph Strobl
+ * @since 5.0
+ */
+record StringUpdate(String raw) {
+
+	String getUpdateString() {
+		return raw;
+	}
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/UpdateInteraction.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/UpdateInteraction.java
new file mode 100644
index 0000000000..525a4782a5
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/UpdateInteraction.java
@@ -0,0 +1,82 @@
+/*
+ * 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.mongodb.repository.aot;
+
+import java.util.Map;
+
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.data.repository.aot.generate.QueryMetadata;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link MongoInteraction} to execute an update.
+ *
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ * @since 5.0
+ */
+class UpdateInteraction extends MongoInteraction implements QueryMetadata {
+
+	private final QueryInteraction filter;
+	private final @Nullable StringUpdate update;
+	private final @Nullable Integer updateDefinitionParameter;
+
+	UpdateInteraction(QueryInteraction filter, @Nullable StringUpdate update,
+			@Nullable Integer updateDefinitionParameter) {
+		this.filter = filter;
+		this.update = update;
+		this.updateDefinitionParameter = updateDefinitionParameter;
+	}
+
+	public QueryInteraction getFilter() {
+		return filter;
+	}
+
+	public @Nullable StringUpdate getUpdate() {
+		return update;
+	}
+
+	public int getRequiredUpdateDefinitionParameter() {
+
+		Assert.notNull(updateDefinitionParameter, "UpdateDefinitionParameter must not be null!");
+
+		return updateDefinitionParameter;
+	}
+
+	public boolean hasUpdateDefinitionParameter() {
+		return updateDefinitionParameter != null;
+	}
+
+	@Override
+	public Map<String, Object> serialize() {
+
+		Map<String, Object> serialized = filter.serialize();
+
+		if (update != null) {
+			serialized.put("filter", filter.getQuery().getQueryString());
+			serialized.put("update", update.getUpdateString());
+		}
+
+		return serialized;
+	}
+
+	@Override
+	InteractionType getExecutionType() {
+		return InteractionType.UPDATE;
+	}
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/package-info.java
index 9016519d9b..750cc38678 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/package-info.java
@@ -1,5 +1,5 @@
 /**
  * Ahead-Of-Time processors for MongoDB repositories.
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 package org.springframework.data.mongodb.repository.aot;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/cdi/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/cdi/package-info.java
index a2cbf659dd..db7edc05bd 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/cdi/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/cdi/package-info.java
@@ -1,6 +1,6 @@
 /**
  * CDI support for MongoDB specific repository implementation.
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 package org.springframework.data.mongodb.repository.cdi;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/MongoRepositoryConfigurationExtension.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/MongoRepositoryConfigurationExtension.java
index 9db7be0069..48b4000750 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/MongoRepositoryConfigurationExtension.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/MongoRepositoryConfigurationExtension.java
@@ -23,10 +23,11 @@
 import org.springframework.beans.factory.support.BeanDefinitionBuilder;
 import org.springframework.core.annotation.AnnotationAttributes;
 import org.springframework.data.config.ParsingUtils;
-import org.springframework.data.mongodb.repository.aot.AotMongoRepositoryPostProcessor;
 import org.springframework.data.mongodb.core.mapping.Document;
 import org.springframework.data.mongodb.repository.MongoRepository;
+import org.springframework.data.mongodb.repository.aot.AotMongoRepositoryPostProcessor;
 import org.springframework.data.mongodb.repository.support.MongoRepositoryFactoryBean;
+import org.springframework.data.mongodb.repository.support.SimpleMongoRepository;
 import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource;
 import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport;
 import org.springframework.data.repository.config.XmlRepositoryConfigurationSource;
@@ -55,6 +56,12 @@ public String getModulePrefix() {
 		return "mongo";
 	}
 
+	@Override
+	public String getRepositoryBaseClassName() {
+		return SimpleMongoRepository.class.getName();
+	}
+
+	@Override
 	public String getRepositoryFactoryBeanClassName() {
 		return MongoRepositoryFactoryBean.class.getName();
 	}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/ReactiveMongoRepositoryConfigurationExtension.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/ReactiveMongoRepositoryConfigurationExtension.java
index 817cc397c2..457e889bef 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/ReactiveMongoRepositoryConfigurationExtension.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/ReactiveMongoRepositoryConfigurationExtension.java
@@ -23,10 +23,12 @@
 import org.springframework.data.config.ParsingUtils;
 import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
 import org.springframework.data.mongodb.repository.support.ReactiveMongoRepositoryFactoryBean;
+import org.springframework.data.mongodb.repository.support.SimpleReactiveMongoRepository;
 import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource;
 import org.springframework.data.repository.config.RepositoryConfigurationExtension;
 import org.springframework.data.repository.config.XmlRepositoryConfigurationSource;
 import org.springframework.data.repository.core.RepositoryMetadata;
+
 import org.w3c.dom.Element;
 
 /**
@@ -47,7 +49,13 @@ public String getModuleName() {
 		return "Reactive MongoDB";
 	}
 
-	public String getRepositoryFactoryClassName() {
+	@Override
+	public String getRepositoryBaseClassName() {
+		return SimpleReactiveMongoRepository.class.getName();
+	}
+
+	@Override
+	public String getRepositoryFactoryBeanClassName() {
 		return ReactiveMongoRepositoryFactoryBean.class.getName();
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/package-info.java
index d0d9b07081..e276d4d1e0 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/package-info.java
@@ -1,6 +1,6 @@
 /**
  * Support infrastructure for the configuration of MongoDB specific repositories.
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 package org.springframework.data.mongodb.repository.config;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/package-info.java
index 8deddfe939..799597e19c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/package-info.java
@@ -1,6 +1,6 @@
 /**
  * MongoDB specific repository implementation.
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 package org.springframework.data.mongodb.repository;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java
index 4d0d604a27..596b895ebd 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java
@@ -20,16 +20,14 @@
 
 import org.bson.Document;
 import org.bson.codecs.configuration.CodecRegistry;
-
-import org.springframework.core.env.StandardEnvironment;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.expression.ValueEvaluationContextProvider;
 import org.springframework.data.expression.ValueExpression;
-import org.springframework.data.expression.ValueExpressionParser;
-import org.springframework.data.mapping.model.SpELExpressionEvaluator;
 import org.springframework.data.mapping.model.ValueExpressionEvaluator;
 import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind;
 import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery;
 import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind;
+import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove;
 import org.springframework.data.mongodb.core.ExecutableUpdateOperation.ExecutableUpdate;
 import org.springframework.data.mongodb.core.MongoOperations;
 import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
@@ -47,17 +45,10 @@
 import org.springframework.data.mongodb.util.json.ParameterBindingContext;
 import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec;
 import org.springframework.data.repository.query.ParameterAccessor;
-import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
-import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor;
 import org.springframework.data.repository.query.RepositoryQuery;
 import org.springframework.data.repository.query.ResultProcessor;
 import org.springframework.data.repository.query.ValueExpressionDelegate;
-import org.springframework.data.spel.ExpressionDependencies;
 import org.springframework.data.util.Lazy;
-import org.springframework.expression.EvaluationContext;
-import org.springframework.expression.ExpressionParser;
-import org.springframework.expression.spel.standard.SpelExpressionParser;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
@@ -79,41 +70,12 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
 	private final MongoOperations operations;
 	private final ExecutableFind<?> executableFind;
 	private final ExecutableUpdate<?> executableUpdate;
+	private final ExecutableRemove<?> executableRemove;
 	private final Lazy<ParameterBindingDocumentCodec> codec = Lazy
 			.of(() -> new ParameterBindingDocumentCodec(getCodecRegistry()));
 	private final ValueExpressionDelegate valueExpressionDelegate;
 	private final ValueEvaluationContextProvider valueEvaluationContextProvider;
 
-	/**
-	 * Creates a new {@link AbstractMongoQuery} from the given {@link MongoQueryMethod} and {@link MongoOperations}.
-	 *
-	 * @param method must not be {@literal null}.
-	 * @param operations must not be {@literal null}.
-	 * @param expressionParser must not be {@literal null}.
-	 * @param evaluationContextProvider must not be {@literal null}.
-	 * @deprecated use the constructor version with {@link ValueExpressionDelegate}
-	 */
-	@Deprecated(since = "4.4.0")
-	public AbstractMongoQuery(MongoQueryMethod method, MongoOperations operations, ExpressionParser expressionParser,
-			QueryMethodEvaluationContextProvider evaluationContextProvider) {
-
-		Assert.notNull(operations, "MongoOperations must not be null");
-		Assert.notNull(method, "MongoQueryMethod must not be null");
-		Assert.notNull(expressionParser, "SpelExpressionParser must not be null");
-		Assert.notNull(evaluationContextProvider, "QueryMethodEvaluationContextProvider must not be null");
-
-		this.method = method;
-		this.operations = operations;
-
-		MongoEntityMetadata<?> metadata = method.getEntityInformation();
-		Class<?> type = metadata.getCollectionEntity().getType();
-
-		this.executableFind = operations.query(type);
-		this.executableUpdate = operations.update(type);
-		this.valueExpressionDelegate = new ValueExpressionDelegate(new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), evaluationContextProvider.getEvaluationContextProvider()), ValueExpressionParser.create(() -> expressionParser));
-		this.valueEvaluationContextProvider = valueExpressionDelegate.createValueContextProvider(method.getParameters());
-	}
-
 	/**
 	 * Creates a new {@link AbstractMongoQuery} from the given {@link MongoQueryMethod} and {@link MongoOperations}.
 	 *
@@ -135,6 +97,7 @@ public AbstractMongoQuery(MongoQueryMethod method, MongoOperations operations, V
 
 		this.executableFind = operations.query(type);
 		this.executableUpdate = operations.update(type);
+		this.executableRemove = operations.remove(type);
 		this.valueExpressionDelegate = delegate;
 		this.valueEvaluationContextProvider = delegate.createValueContextProvider(method.getParameters());
 	}
@@ -145,7 +108,7 @@ public MongoQueryMethod getQueryMethod() {
 	}
 
 	@Override
-	public Object execute(Object[] parameters) {
+	public @Nullable Object execute(Object[] parameters) {
 
 		ConvertingParameterAccessor accessor = new ConvertingParameterAccessor(operations.getConverter(),
 				new MongoParametersParameterAccessor(method, parameters));
@@ -165,8 +128,7 @@ public Object execute(Object[] parameters) {
 	 * @param accessor for providing invocation arguments. Never {@literal null}.
 	 * @param typeToRead the desired component target type. Can be {@literal null}.
 	 */
-	@Nullable
-	protected Object doExecute(MongoQueryMethod method, ResultProcessor processor, ConvertingParameterAccessor accessor,
+	protected @Nullable Object doExecute(MongoQueryMethod method, ResultProcessor processor, ConvertingParameterAccessor accessor,
 			@Nullable Class<?> typeToRead) {
 
 		Query query = createQuery(accessor);
@@ -185,7 +147,8 @@ protected Object doExecute(MongoQueryMethod method, ResultProcessor processor, C
 	}
 
 	/**
-	 * If present apply the {@link com.mongodb.ReadPreference} from the {@link org.springframework.data.mongodb.repository.ReadPreference} annotation.
+	 * If present apply the {@link com.mongodb.ReadPreference} from the
+	 * {@link org.springframework.data.mongodb.repository.ReadPreference} annotation.
 	 *
 	 * @param query must not be {@literal null}.
 	 * @return never {@literal null}.
@@ -200,10 +163,11 @@ private Query applyAnnotatedReadPreferenceIfPresent(Query query) {
 		return query.withReadPreference(com.mongodb.ReadPreference.valueOf(method.getAnnotatedReadPreference()));
 	}
 
-	private MongoQueryExecution getExecution(ConvertingParameterAccessor accessor, FindWithQuery<?> operation) {
+	@SuppressWarnings("NullAway")
+	MongoQueryExecution getExecution(ConvertingParameterAccessor accessor, FindWithQuery<?> operation) {
 
 		if (isDeleteQuery()) {
-			return new DeleteExecution(operations, method);
+			return new DeleteExecution<>(executableRemove, method);
 		}
 
 		if (method.isModifyingQuery()) {
@@ -320,6 +284,7 @@ protected Query createCountQuery(ConvertingParameterAccessor accessor) {
 	 * @throws IllegalStateException if no update could be found.
 	 * @since 3.4
 	 */
+	@SuppressWarnings("NullAway")
 	protected UpdateDefinition createUpdate(ConvertingParameterAccessor accessor) {
 
 		if (accessor.getUpdate() != null) {
@@ -380,7 +345,7 @@ private Document bindParameters(String source, ConvertingParameterAccessor acces
 	 * @return never {@literal null}.
 	 * @since 3.4
 	 */
-	protected ParameterBindingContext prepareBindingContext(String source, ConvertingParameterAccessor accessor) {
+	protected ParameterBindingContext prepareBindingContext(String source, MongoParameterAccessor accessor) {
 
 		ValueExpressionEvaluator evaluator = getExpressionEvaluatorFor(accessor);
 		return new ParameterBindingContext(accessor::getBindableValue, evaluator);
@@ -396,20 +361,6 @@ protected ParameterBindingDocumentCodec getParameterBindingCodec() {
 		return codec.get();
 	}
 
-	/**
-	 * Obtain a the {@link EvaluationContext} suitable to evaluate expressions backed by the given dependencies.
-	 *
-	 * @param dependencies must not be {@literal null}.
-	 * @param accessor must not be {@literal null}.
-	 * @return the {@link SpELExpressionEvaluator}.
-	 * @since 2.4
-	 */
-	protected SpELExpressionEvaluator getSpELExpressionEvaluatorFor(ExpressionDependencies dependencies,
-			ConvertingParameterAccessor accessor) {
-
-		return new DefaultSpELExpressionEvaluator(new SpelExpressionParser(), valueEvaluationContextProvider.getEvaluationContext(accessor.getValues(), dependencies).getEvaluationContext());
-	}
-
 	/**
 	 * Obtain a {@link ValueExpressionEvaluator} suitable to evaluate expressions.
 	 *
@@ -418,14 +369,16 @@ protected SpELExpressionEvaluator getSpELExpressionEvaluatorFor(ExpressionDepend
 	 * @since 4.4.0
 	 */
 	protected ValueExpressionEvaluator getExpressionEvaluatorFor(MongoParameterAccessor accessor) {
-		return new ValueExpressionDelegateValueExpressionEvaluator(valueExpressionDelegate, (ValueExpression expression) ->
-				valueEvaluationContextProvider.getEvaluationContext(accessor.getValues(), expression.getExpressionDependencies()));
+		return new ValueExpressionDelegateValueExpressionEvaluator(valueExpressionDelegate,
+				(ValueExpression expression) -> valueEvaluationContextProvider.getEvaluationContext(accessor.getValues(),
+						expression.getExpressionDependencies()));
 	}
 
 	/**
 	 * @return the {@link CodecRegistry} used.
 	 * @since 2.4
 	 */
+	@SuppressWarnings("NullAway")
 	protected CodecRegistry getCodecRegistry() {
 		return operations.execute(MongoDatabase::getCodecRegistry);
 	}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java
index a5754a4e46..d363c93442 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java
@@ -24,17 +24,15 @@
 
 import org.bson.Document;
 import org.bson.codecs.configuration.CodecRegistry;
+import org.jspecify.annotations.Nullable;
 import org.reactivestreams.Publisher;
 
 import org.springframework.core.convert.converter.Converter;
-import org.springframework.core.env.StandardEnvironment;
 import org.springframework.data.expression.ReactiveValueEvaluationContextProvider;
 import org.springframework.data.expression.ValueEvaluationContext;
 import org.springframework.data.expression.ValueEvaluationContextProvider;
 import org.springframework.data.expression.ValueExpression;
-import org.springframework.data.expression.ValueExpressionParser;
 import org.springframework.data.mapping.model.EntityInstantiators;
-import org.springframework.data.mapping.model.SpELExpressionEvaluator;
 import org.springframework.data.mapping.model.ValueExpressionEvaluator;
 import org.springframework.data.mongodb.core.MongoOperations;
 import org.springframework.data.mongodb.core.ReactiveFindOperation.FindWithProjection;
@@ -56,15 +54,10 @@
 import org.springframework.data.mongodb.util.json.ParameterBindingContext;
 import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec;
 import org.springframework.data.repository.query.ParameterAccessor;
-import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor;
-import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
 import org.springframework.data.repository.query.RepositoryQuery;
 import org.springframework.data.repository.query.ResultProcessor;
 import org.springframework.data.repository.query.ValueExpressionDelegate;
 import org.springframework.data.spel.ExpressionDependencies;
-import org.springframework.expression.ExpressionParser;
-import org.springframework.expression.spel.standard.SpelExpressionParser;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
@@ -89,45 +82,6 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
 	private final ValueExpressionDelegate valueExpressionDelegate;
 	private final ReactiveValueEvaluationContextProvider valueEvaluationContextProvider;
 
-	/**
-	 * Creates a new {@link AbstractReactiveMongoQuery} from the given {@link MongoQueryMethod} and
-	 * {@link MongoOperations}.
-	 *
-	 * @param method must not be {@literal null}.
-	 * @param operations must not be {@literal null}.
-	 * @param expressionParser must not be {@literal null}.
-	 * @param evaluationContextProvider must not be {@literal null}.
-	 * @deprecated use the constructor version with {@link ValueExpressionDelegate}
-	 */
-	@Deprecated(since = "4.4.0")
-	public AbstractReactiveMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations operations,
-			ExpressionParser expressionParser, ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) {
-
-		Assert.notNull(method, "MongoQueryMethod must not be null");
-		Assert.notNull(operations, "ReactiveMongoOperations must not be null");
-		Assert.notNull(expressionParser, "SpelExpressionParser must not be null");
-		Assert.notNull(evaluationContextProvider, "ReactiveEvaluationContextExtension must not be null");
-
-		this.method = method;
-		this.operations = operations;
-		this.instantiators = new EntityInstantiators();
-		this.valueExpressionDelegate = new ValueExpressionDelegate(
-				new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(),
-						evaluationContextProvider.getEvaluationContextProvider()),
-				ValueExpressionParser.create(() -> expressionParser));
-
-		MongoEntityMetadata<?> metadata = method.getEntityInformation();
-		Class<?> type = metadata.getCollectionEntity().getType();
-
-		this.findOperationWithProjection = operations.query(type);
-		this.updateOps = operations.update(type);
-		ValueEvaluationContextProvider valueContextProvider = valueExpressionDelegate
-				.createValueContextProvider(method.getParameters());
-		Assert.isInstanceOf(ReactiveValueEvaluationContextProvider.class, valueContextProvider,
-				"ValueEvaluationContextProvider must be reactive");
-		this.valueEvaluationContextProvider = (ReactiveValueEvaluationContextProvider) valueContextProvider;
-	}
-
 	/**
 	 * Creates a new {@link AbstractReactiveMongoQuery} from the given {@link MongoQueryMethod} and
 	 * {@link MongoOperations}.
@@ -241,6 +195,7 @@ private ReactiveMongoQueryExecution getExecution(MongoParameterAccessor accessor
 		return new ResultProcessingExecution(getExecutionToWrap(accessor, operation), resultProcessing);
 	}
 
+	@SuppressWarnings("NullAway")
 	private ReactiveMongoQueryExecution getExecutionToWrap(MongoParameterAccessor accessor, FindWithQuery<?> operation) {
 
 		if (isDeleteQuery()) {
@@ -380,6 +335,7 @@ protected Mono<Query> createCountQuery(ConvertingParameterAccessor accessor) {
 	 * @throws IllegalStateException if no update could be found.
 	 * @since 3.4
 	 */
+	@SuppressWarnings("NullAway")
 	protected Mono<UpdateDefinition> createUpdate(MongoParameterAccessor accessor) {
 
 		if (accessor.getUpdate() != null) {
@@ -460,26 +416,6 @@ protected Mono<ParameterBindingDocumentCodec> getParameterBindingCodec() {
 		return getCodecRegistry().map(ParameterBindingDocumentCodec::new);
 	}
 
-	/**
-	 * Obtain a {@link Mono publisher} emitting the {@link SpELExpressionEvaluator} suitable to evaluate expressions
-	 * backed by the given dependencies.
-	 *
-	 * @param dependencies must not be {@literal null}.
-	 * @param accessor must not be {@literal null}.
-	 * @return a {@link Mono} emitting the {@link SpELExpressionEvaluator} when ready.
-	 * @since 3.4
-	 * @deprecated since 4.4.0, use
-	 *             {@link #getValueExpressionEvaluatorLater(ExpressionDependencies, MongoParameterAccessor)} instead
-	 */
-	@Deprecated(since = "4.4.0")
-	protected Mono<SpELExpressionEvaluator> getSpelEvaluatorFor(ExpressionDependencies dependencies,
-			MongoParameterAccessor accessor) {
-		return valueEvaluationContextProvider.getEvaluationContextLater(accessor.getValues(), dependencies)
-				.map(evaluationContext -> (SpELExpressionEvaluator) new DefaultSpELExpressionEvaluator(
-						new SpelExpressionParser(), evaluationContext.getEvaluationContext()))
-				.defaultIfEmpty(DefaultSpELExpressionEvaluator.unsupported());
-	}
-
 	/**
 	 * Obtain a {@link ValueExpressionEvaluator} suitable to evaluate expressions.
 	 *
@@ -491,7 +427,7 @@ ValueExpressionEvaluator getValueExpressionEvaluator(MongoParameterAccessor acce
 		return new ValueExpressionEvaluator() {
 
 			@Override
-			public <T> T evaluate(String expressionString) {
+			public <T> @Nullable T evaluate(String expressionString) {
 				ValueExpression expression = valueExpressionDelegate.parse(expressionString);
 				ValueEvaluationContext evaluationContext = valueEvaluationContextProvider
 						.getEvaluationContext(accessor.getValues(), expression.getExpressionDependencies());
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java
index 6eb6a5da89..639c694ef9 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java
@@ -22,7 +22,7 @@
 import java.util.function.LongUnaryOperator;
 
 import org.bson.Document;
-
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.Sort.Order;
 import org.springframework.data.mapping.model.ValueExpressionEvaluator;
@@ -41,7 +41,6 @@
 import org.springframework.data.repository.query.ReturnedType;
 import org.springframework.data.util.ReflectionUtils;
 import org.springframework.data.util.TypeInformation;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.ObjectUtils;
 
@@ -166,8 +165,7 @@ static AggregationOptions computeOptions(MongoQueryMethod method, ConvertingPara
 	 * Prepares the AggregationPipeline including type discovery and calling {@link AggregationCallback} to run the
 	 * aggregation.
 	 */
-	@Nullable
-	static <T> T doAggregate(AggregationPipeline pipeline, MongoQueryMethod method, ResultProcessor processor,
+	static <T> @Nullable T doAggregate(AggregationPipeline pipeline, MongoQueryMethod method, ResultProcessor processor,
 			ConvertingParameterAccessor accessor,
 			Function<MongoParameterAccessor, ValueExpressionEvaluator> evaluatorFunction, AggregationCallback<T> callback) {
 
@@ -308,8 +306,7 @@ static void appendLimitAndOffsetIfPresent(AggregationPipeline aggregationPipelin
 	 * @return can be {@literal null} if source {@link Document#isEmpty() is empty}.
 	 * @throws IllegalArgumentException when none of the above rules is met.
 	 */
-	@Nullable
-	static <T> T extractSimpleTypeResult(@Nullable Document source, Class<T> targetType, MongoConverter converter) {
+	static <T> @Nullable T extractSimpleTypeResult(@Nullable Document source, Class<T> targetType, MongoConverter converter) {
 
 		if (ObjectUtils.isEmpty(source)) {
 			return null;
@@ -336,9 +333,8 @@ static <T> T extractSimpleTypeResult(@Nullable Document source, Class<T> targetT
 				String.format("o_O no entry of type %s found in %s.", targetType.getSimpleName(), source.toJson()));
 	}
 
-	@Nullable
 	@SuppressWarnings("unchecked")
-	private static <T> T getPotentiallyConvertedSimpleTypeValue(MongoConverter converter, @Nullable Object value,
+	private static <T> @Nullable T getPotentiallyConvertedSimpleTypeValue(MongoConverter converter, @Nullable Object value,
 			Class<T> targetType) {
 
 		if (value == null) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/CollationUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/CollationUtils.java
index 2aac6b77a8..108c6ee796 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/CollationUtils.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/CollationUtils.java
@@ -20,12 +20,12 @@
 import java.util.regex.Pattern;
 
 import org.bson.Document;
-
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mapping.model.ValueExpressionEvaluator;
 import org.springframework.data.mongodb.core.query.Collation;
 import org.springframework.data.mongodb.util.json.ParameterBindingContext;
 import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec;
-import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
 import org.springframework.util.NumberUtils;
 import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
@@ -55,8 +55,7 @@ private CollationUtils() {
 	 * @return can be {@literal null} if neither {@link ConvertingParameterAccessor#getCollation()} nor
 	 *         {@literal collationExpression} are present.
 	 */
-	@Nullable
-	static Collation computeCollation(@Nullable String collationExpression, ConvertingParameterAccessor accessor,
+	static @Nullable Collation computeCollation(@Nullable String collationExpression, ConvertingParameterAccessor accessor,
 			ValueExpressionEvaluator expressionEvaluator) {
 
 		if (accessor.getCollation() != null) {
@@ -98,6 +97,7 @@ static Collation computeCollation(@Nullable String collationExpression, Converti
 					ObjectUtils.nullSafeClassName(placeholderValue)));
 		}
 
+		Assert.notNull(placeholderValue, "PlaceholderValue must not be null");
 		return Collation.parse(collationExpression.replace(placeholder, placeholderValue.toString()));
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java
index dbf87f2f2e..f203b67e67 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java
@@ -21,11 +21,15 @@
 import java.util.Iterator;
 import java.util.List;
 
+import org.jspecify.annotations.Nullable;
+
 import org.springframework.data.domain.Limit;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.Range;
+import org.springframework.data.domain.Score;
 import org.springframework.data.domain.ScrollPosition;
 import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Vector;
 import org.springframework.data.geo.Distance;
 import org.springframework.data.geo.Point;
 import org.springframework.data.mongodb.core.convert.MongoWriter;
@@ -35,7 +39,6 @@
 import org.springframework.data.mongodb.core.query.UpdateDefinition;
 import org.springframework.data.repository.query.ParameterAccessor;
 import org.springframework.data.util.TypeInformation;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.CollectionUtils;
 
@@ -74,7 +77,12 @@ public PotentiallyConvertingIterator iterator() {
 	}
 
 	@Override
-	public ScrollPosition getScrollPosition() {
+	public @Nullable Vector getVector() {
+		return delegate.getVector();
+	}
+
+	@Override
+	public @Nullable ScrollPosition getScrollPosition() {
 		return delegate.getScrollPosition();
 	}
 
@@ -87,34 +95,44 @@ public Sort getSort() {
 	}
 
 	@Override
-	public Class<?> findDynamicProjection() {
+	public @Nullable Class<?> findDynamicProjection() {
 		return delegate.findDynamicProjection();
 	}
 
-	public Object getBindableValue(int index) {
+	public @Nullable Object getBindableValue(int index) {
 		return getConvertedValue(delegate.getBindableValue(index), null);
 	}
 
 	@Override
-	public Range<Distance> getDistanceRange() {
+	public @Nullable Score getScore() {
+		return delegate.getScore();
+	}
+
+	@Override
+	public @Nullable Range<Score> getScoreRange() {
+		return delegate.getScoreRange();
+	}
+
+	@Override
+	public @Nullable Range<Distance> getDistanceRange() {
 		return delegate.getDistanceRange();
 	}
 
-	public Point getGeoNearLocation() {
+	public @Nullable Point getGeoNearLocation() {
 		return delegate.getGeoNearLocation();
 	}
 
-	public TextCriteria getFullText() {
+	public @Nullable TextCriteria getFullText() {
 		return delegate.getFullText();
 	}
 
 	@Override
-	public Collation getCollation() {
+	public @Nullable Collation getCollation() {
 		return delegate.getCollation();
 	}
 
 	@Override
-	public UpdateDefinition getUpdate() {
+	public @Nullable UpdateDefinition getUpdate() {
 		return delegate.getUpdate();
 	}
 
@@ -130,8 +148,7 @@ public Limit getLimit() {
 	 * @param typeInformation can be {@literal null}.
 	 * @return can be {@literal null}.
 	 */
-	@Nullable
-	private Object getConvertedValue(Object value, @Nullable TypeInformation<?> typeInformation) {
+	private @Nullable Object getConvertedValue(@Nullable Object value, @Nullable TypeInformation<?> typeInformation) {
 		return writer.convertToMongoType(value, typeInformation == null ? null : typeInformation.getActualType());
 	}
 
@@ -161,11 +178,11 @@ public boolean hasNext() {
 			return delegate.hasNext();
 		}
 
-		public Object next() {
+		public @Nullable Object next() {
 			return delegate.next();
 		}
 
-		public Object nextConverted(MongoPersistentProperty property) {
+		public @Nullable Object nextConverted(MongoPersistentProperty property) {
 
 			Object next = next();
 
@@ -209,7 +226,7 @@ private static Collection<?> asCollection(@Nullable Object source) {
 
 		if (source instanceof Iterable<?> iterable) {
 
-			if(source instanceof Collection<?> collection) {
+			if (source instanceof Collection<?> collection) {
 				return new ArrayList<>(collection);
 			}
 
@@ -228,7 +245,7 @@ private static Collection<?> asCollection(@Nullable Object source) {
 	}
 
 	@Override
-	public Object[] getValues() {
+	public Object @Nullable[] getValues() {
 		return delegate.getValues();
 	}
 
@@ -244,6 +261,6 @@ public interface PotentiallyConvertingIterator extends Iterator<Object> {
 		 *
 		 * @return
 		 */
-		Object nextConverted(MongoPersistentProperty property);
+		@Nullable Object nextConverted(MongoPersistentProperty property);
 	}
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/DefaultSpELExpressionEvaluator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/DefaultSpELExpressionEvaluator.java
deleted file mode 100644
index 16a1e55226..0000000000
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/DefaultSpELExpressionEvaluator.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright 2020-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.mongodb.repository.query;
-
-import org.springframework.data.mapping.model.SpELExpressionEvaluator;
-import org.springframework.expression.EvaluationContext;
-import org.springframework.expression.ExpressionParser;
-
-/**
- * Simple {@link SpELExpressionEvaluator} implementation using {@link ExpressionParser} and {@link EvaluationContext}.
- *
- * @author Mark Paluch
- * @since 3.1
- */
-class DefaultSpELExpressionEvaluator implements SpELExpressionEvaluator {
-
-	private final ExpressionParser parser;
-	private final EvaluationContext context;
-
-	DefaultSpELExpressionEvaluator(ExpressionParser parser, EvaluationContext context) {
-		this.parser = parser;
-		this.context = context;
-	}
-
-	/**
-	 * Return a {@link SpELExpressionEvaluator} that does not support expression evaluation.
-	 *
-	 * @return a {@link SpELExpressionEvaluator} that does not support expression evaluation.
-	 * @since 3.1
-	 */
-	public static SpELExpressionEvaluator unsupported() {
-		return NoOpExpressionEvaluator.INSTANCE;
-	}
-
-	@Override
-	@SuppressWarnings("unchecked")
-	public <T> T evaluate(String expression) {
-		return (T) parser.parseExpression(expression).getValue(context, Object.class);
-	}
-
-	/**
-	 * {@link SpELExpressionEvaluator} that does not support SpEL evaluation.
-	 *
-	 * @author Mark Paluch
-	 * @since 3.1
-	 */
-	enum NoOpExpressionEvaluator implements SpELExpressionEvaluator {
-
-		INSTANCE;
-
-		@Override
-		public <T> T evaluate(String expression) {
-			throw new UnsupportedOperationException("Expression evaluation not supported");
-		}
-	}
-}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoEntityInformation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoEntityInformation.java
index 8678e5a74c..c54d689b52 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoEntityInformation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoEntityInformation.java
@@ -15,9 +15,9 @@
  */
 package org.springframework.data.mongodb.repository.query;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.query.Collation;
 import org.springframework.data.repository.core.EntityInformation;
-import org.springframework.lang.Nullable;
 
 /**
  * Mongo specific {@link EntityInformation}.
@@ -58,8 +58,7 @@ default boolean isVersioned() {
 	 * @return can be {@literal null}.
 	 * @since 2.2
 	 */
-	@Nullable
-	default Object getVersion(T entity) {
+	default @Nullable Object getVersion(T entity) {
 		return null;
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java
index 5db853e810..1b52233eac 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java
@@ -15,6 +15,8 @@
  */
 package org.springframework.data.mongodb.repository.query;
 
+import org.jspecify.annotations.Nullable;
+
 import org.springframework.data.domain.Range;
 import org.springframework.data.geo.Distance;
 import org.springframework.data.geo.Point;
@@ -23,7 +25,6 @@
 import org.springframework.data.mongodb.core.query.Update;
 import org.springframework.data.mongodb.core.query.UpdateDefinition;
 import org.springframework.data.repository.query.ParameterAccessor;
-import org.springframework.lang.Nullable;
 
 /**
  * Mongo-specific {@link ParameterAccessor} exposing a maximum distance parameter.
@@ -41,7 +42,7 @@ public interface MongoParameterAccessor extends ParameterAccessor {
 	 * @return the maximum distance to apply to the geo query or {@literal null} if there's no {@link Distance} parameter
 	 *         at all or the given value for it was {@literal null}.
 	 */
-	Range<Distance> getDistanceRange();
+	@Nullable Range<Distance> getDistanceRange();
 
 	/**
 	 * Returns the {@link Point} to use for a geo-near query.
@@ -75,7 +76,7 @@ public interface MongoParameterAccessor extends ParameterAccessor {
 	 * @return
 	 * @since 1.8
 	 */
-	Object[] getValues();
+	Object @Nullable[] getValues();
 
 	/**
 	 * Returns the {@link Update} to be used for an update execution.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java
index 1f66d5b77d..94acef17ce 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java
@@ -20,8 +20,11 @@
 import java.util.Arrays;
 import java.util.List;
 
+import org.jspecify.annotations.Nullable;
+
 import org.springframework.core.MethodParameter;
 import org.springframework.data.domain.Range;
+import org.springframework.data.domain.Vector;
 import org.springframework.data.geo.Distance;
 import org.springframework.data.geo.GeoPage;
 import org.springframework.data.geo.GeoResult;
@@ -36,7 +39,6 @@
 import org.springframework.data.repository.query.Parameters;
 import org.springframework.data.repository.query.ParametersSource;
 import org.springframework.data.util.TypeInformation;
-import org.springframework.lang.Nullable;
 
 /**
  * Custom extension of {@link Parameters} discovering additional
@@ -53,9 +55,9 @@ public class MongoParameters extends Parameters<MongoParameters, MongoParameter>
 
 	private final int rangeIndex;
 	private final int maxDistanceIndex;
-	private final @Nullable Integer fullTextIndex;
-	private final @Nullable Integer nearIndex;
-	private final @Nullable Integer collationIndex;
+	private final int fullTextIndex;
+	private final int nearIndex;
+	private final int collationIndex;
 	private final int updateIndex;
 	private final TypeInformation<?> domainType;
 
@@ -106,9 +108,8 @@ private MongoParameters(ParametersSource parametersSource, NearIndex nearIndex)
 		this.nearIndex = nearIndex.nearIndex;
 	}
 
-	private MongoParameters(List<MongoParameter> parameters, int maxDistanceIndex, @Nullable Integer nearIndex,
-			@Nullable Integer fullTextIndex, int rangeIndex, @Nullable Integer collationIndex, int updateIndex,
-			TypeInformation<?> domainType) {
+	private MongoParameters(List<MongoParameter> parameters, int maxDistanceIndex, int nearIndex, int fullTextIndex,
+			int rangeIndex, int collationIndex, int updateIndex, TypeInformation<?> domainType) {
 
 		super(parameters);
 
@@ -141,7 +142,7 @@ static boolean isGeoNearQuery(Method method) {
 
 	static class NearIndex {
 
-		private final @Nullable Integer nearIndex;
+		private final int nearIndex;
 
 		public NearIndex(ParametersSource parametersSource, boolean isGeoNearMethod) {
 
@@ -196,10 +197,6 @@ static int findNearIndexInParameters(Method method) {
 		return index;
 	}
 
-	public int getDistanceRangeIndex() {
-		return -1;
-	}
-
 	/**
 	 * Returns the index of the {@link Distance} parameter to be used for max distance in geo queries.
 	 *
@@ -226,7 +223,7 @@ public int getNearIndex() {
 	 * @since 1.6
 	 */
 	public int getFullTextParameterIndex() {
-		return fullTextIndex != null ? fullTextIndex : -1;
+		return fullTextIndex;
 	}
 
 	/**
@@ -234,7 +231,7 @@ public int getFullTextParameterIndex() {
 	 * @since 1.6
 	 */
 	public boolean hasFullTextParameter() {
-		return this.fullTextIndex != null && this.fullTextIndex >= 0;
+		return this.fullTextIndex >= 0;
 	}
 
 	/**
@@ -252,7 +249,7 @@ public int getRangeIndex() {
 	 * @since 2.2
 	 */
 	public int getCollationParameterIndex() {
-		return collationIndex != null ? collationIndex : -1;
+		return collationIndex;
 	}
 
 	/**
@@ -318,7 +315,8 @@ static class MongoParameter extends Parameter {
 
 		@Override
 		public boolean isSpecialParameter() {
-			return super.isSpecialParameter() || Distance.class.isAssignableFrom(getType()) || isNearParameter()
+			return super.isSpecialParameter() || Distance.class.isAssignableFrom(getType())
+					|| Vector.class.isAssignableFrom(getType()) || isNearParameter()
 					|| TextCriteria.class.isAssignableFrom(getType()) || Collation.class.isAssignableFrom(getType());
 		}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java
index ac1931e10c..0f56223492 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java
@@ -15,8 +15,11 @@
  */
 package org.springframework.data.mongodb.repository.query;
 
+import org.jspecify.annotations.Nullable;
+
 import org.springframework.data.domain.Range;
 import org.springframework.data.domain.Range.Bound;
+import org.springframework.data.domain.Score;
 import org.springframework.data.geo.Distance;
 import org.springframework.data.geo.Point;
 import org.springframework.data.mongodb.core.query.Collation;
@@ -24,7 +27,7 @@
 import org.springframework.data.mongodb.core.query.TextCriteria;
 import org.springframework.data.mongodb.core.query.UpdateDefinition;
 import org.springframework.data.repository.query.ParametersParameterAccessor;
-import org.springframework.lang.Nullable;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 
@@ -53,6 +56,24 @@ public MongoParametersParameterAccessor(MongoQueryMethod method, Object[] values
 		this.method = method;
 	}
 
+	@SuppressWarnings("NullAway")
+	@Override
+	public Range<Score> getScoreRange() {
+
+		MongoParameters mongoParameters = method.getParameters();
+
+		if (mongoParameters.hasScoreRangeParameter()) {
+			return getValue(mongoParameters.getScoreRangeIndex());
+		}
+
+		Score score = getScore();
+		Bound<Score> maxDistance = score != null ? Bound.inclusive(score) : Bound.unbounded();
+
+		return Range.of(Bound.unbounded(), maxDistance);
+	}
+
+	@SuppressWarnings("NullAway")
+	@Override
 	public Range<Distance> getDistanceRange() {
 
 		MongoParameters mongoParameters = method.getParameters();
@@ -70,7 +91,7 @@ public Range<Distance> getDistanceRange() {
 		return Range.of(Bound.unbounded(), maxDistance);
 	}
 
-	public Point getGeoNearLocation() {
+	public @Nullable Point getGeoNearLocation() {
 
 		int nearIndex = method.getParameters().getNearIndex();
 
@@ -95,14 +116,14 @@ public Point getGeoNearLocation() {
 		return (Point) value;
 	}
 
-	@Nullable
 	@Override
-	public TextCriteria getFullText() {
+	public @Nullable TextCriteria getFullText() {
 		int index = method.getParameters().getFullTextParameterIndex();
 		return index >= 0 ? potentiallyConvertFullText(getValue(index)) : null;
 	}
 
-	protected TextCriteria potentiallyConvertFullText(Object fullText) {
+	@Contract("null -> fail")
+	protected TextCriteria potentiallyConvertFullText(@Nullable Object fullText) {
 
 		Assert.notNull(fullText, "Fulltext parameter must not be 'null'.");
 
@@ -124,7 +145,7 @@ protected TextCriteria potentiallyConvertFullText(Object fullText) {
 	}
 
 	@Override
-	public Collation getCollation() {
+	public @Nullable Collation getCollation() {
 
 		if (method.getParameters().getCollationParameterIndex() == -1) {
 			return null;
@@ -134,12 +155,12 @@ public Collation getCollation() {
 	}
 
 	@Override
-	public Object[] getValues() {
+	public Object @Nullable[] getValues() {
 		return super.getValues();
 	}
 
 	@Override
-	public UpdateDefinition getUpdate() {
+	public @Nullable UpdateDefinition getUpdate() {
 
 		int updateIndex = method.getParameters().getUpdateIndex();
 		return updateIndex == -1 ? null : (UpdateDefinition) getValue(updateIndex);
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java
index 66a8870623..ba7394ec17 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java
@@ -15,7 +15,8 @@
  */
 package org.springframework.data.mongodb.repository.query;
 
-import static org.springframework.data.mongodb.core.query.Criteria.*;
+import static org.springframework.data.mongodb.core.query.Criteria.Placeholder;
+import static org.springframework.data.mongodb.core.query.Criteria.where;
 
 import java.util.Arrays;
 import java.util.Collection;
@@ -26,6 +27,7 @@
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.bson.BsonRegularExpression;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.domain.Range;
 import org.springframework.data.domain.Range.Bound;
 import org.springframework.data.domain.Sort;
@@ -52,7 +54,6 @@
 import org.springframework.data.repository.query.parser.Part.Type;
 import org.springframework.data.repository.query.parser.PartTree;
 import org.springframework.data.util.Streamable;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.ObjectUtils;
@@ -65,13 +66,14 @@
  * @author Christoph Strobl
  * @author Edward Prentice
  */
-class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
+public class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
 
 	private static final Log LOG = LogFactory.getLog(MongoQueryCreator.class);
 
 	private final MongoParameterAccessor accessor;
 	private final MappingContext<?, MongoPersistentProperty> context;
 	private final boolean isGeoNearQuery;
+	private final boolean isSearchQuery;
 
 	/**
 	 * Creates a new {@link MongoQueryCreator} from the given {@link PartTree}, {@link ConvertingParameterAccessor} and
@@ -81,9 +83,9 @@ class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
 	 * @param accessor
 	 * @param context
 	 */
-	public MongoQueryCreator(PartTree tree, ConvertingParameterAccessor accessor,
+	public MongoQueryCreator(PartTree tree, MongoParameterAccessor accessor,
 			MappingContext<?, MongoPersistentProperty> context) {
-		this(tree, accessor, context, false);
+		this(tree, accessor, context, false, false);
 	}
 
 	/**
@@ -94,9 +96,10 @@ public MongoQueryCreator(PartTree tree, ConvertingParameterAccessor accessor,
 	 * @param accessor
 	 * @param context
 	 * @param isGeoNearQuery
+	 * @param isSearchQuery
 	 */
-	public MongoQueryCreator(PartTree tree, ConvertingParameterAccessor accessor,
-			MappingContext<?, MongoPersistentProperty> context, boolean isGeoNearQuery) {
+	public MongoQueryCreator(PartTree tree, MongoParameterAccessor accessor,
+			MappingContext<?, MongoPersistentProperty> context, boolean isGeoNearQuery, boolean isSearchQuery) {
 
 		super(tree, accessor);
 
@@ -104,6 +107,7 @@ public MongoQueryCreator(PartTree tree, ConvertingParameterAccessor accessor,
 
 		this.accessor = accessor;
 		this.isGeoNearQuery = isGeoNearQuery;
+		this.isSearchQuery = isSearchQuery;
 		this.context = context;
 	}
 
@@ -111,7 +115,12 @@ public MongoQueryCreator(PartTree tree, ConvertingParameterAccessor accessor,
 	protected Criteria create(Part part, Iterator<Object> iterator) {
 
 		if (isGeoNearQuery && part.getType().equals(Type.NEAR)) {
-			return null;
+			return new Criteria();
+		}
+
+		if (isPartOfSearchQuery(part)) {
+			skip(part, iterator);
+			return new Criteria();
 		}
 
 		PersistentPropertyPath<MongoPersistentProperty> path = context.getPersistentPropertyPath(part.getProperty());
@@ -127,6 +136,11 @@ protected Criteria and(Part part, Criteria base, Iterator<Object> iterator) {
 			return create(part, iterator);
 		}
 
+		if (isPartOfSearchQuery(part)) {
+			skip(part, iterator);
+			return base;
+		}
+
 		PersistentPropertyPath<MongoPersistentProperty> path = context.getPersistentPropertyPath(part.getProperty());
 		MongoPersistentProperty property = path.getLeafProperty();
 
@@ -141,7 +155,7 @@ protected Criteria or(Criteria base, Criteria criteria) {
 	}
 
 	@Override
-	protected Query complete(Criteria criteria, Sort sort) {
+	protected Query complete(@Nullable Criteria criteria, Sort sort) {
 
 		Query query = (criteria == null ? new Query() : new Query(criteria)).with(sort);
 
@@ -161,6 +175,7 @@ protected Query complete(Criteria criteria, Sort sort) {
 	 * @param parameters
 	 * @return
 	 */
+	@SuppressWarnings("NullAway")
 	private Criteria from(Part part, MongoPersistentProperty property, Criteria criteria, Iterator<Object> parameters) {
 
 		Type type = part.getType();
@@ -183,9 +198,17 @@ private Criteria from(Part part, MongoPersistentProperty property, Criteria crit
 			case IS_NULL:
 				return criteria.is(null);
 			case NOT_IN:
-				return criteria.nin(nextAsList(parameters, part));
+				Object ninValue = parameters.next();
+				if (ninValue instanceof Placeholder) {
+					return criteria.raw("$nin", ninValue);
+				}
+				return criteria.nin(valueAsList(ninValue, part));
 			case IN:
-				return criteria.in(nextAsList(parameters, part));
+				Object inValue = parameters.next();
+				if (inValue instanceof Placeholder) {
+					return criteria.raw("$in", inValue);
+				}
+				return criteria.in(valueAsList(inValue, part));
 			case LIKE:
 			case STARTING_WITH:
 			case ENDING_WITH:
@@ -200,7 +223,12 @@ private Criteria from(Part part, MongoPersistentProperty property, Criteria crit
 				Object param = parameters.next();
 				return param instanceof Pattern pattern ? criteria.regex(pattern) : criteria.regex(param.toString());
 			case EXISTS:
-				return criteria.exists((Boolean) parameters.next());
+				Object next = parameters.next();
+				if (next instanceof Placeholder placeholder) {
+					return criteria.raw("$exists", placeholder);
+				} else {
+					return criteria.exists((Boolean) next);
+				}
 			case TRUE:
 				return criteria.is(true);
 			case FALSE:
@@ -319,7 +347,11 @@ private Criteria createContainingCriteria(Part part, MongoPersistentProperty pro
 			Iterator<Object> parameters) {
 
 		if (property.isCollectionLike()) {
-			return criteria.in(nextAsList(parameters, part));
+			Object next = parameters.next();
+			if (next instanceof Placeholder) {
+				return criteria.raw("$in", next);
+			}
+			return criteria.in(valueAsList(next, part));
 		}
 
 		return addAppropriateLikeRegexTo(criteria, part, parameters.next());
@@ -333,6 +365,7 @@ private Criteria createContainingCriteria(Part part, MongoPersistentProperty pro
 	 * @param value
 	 * @return the criteria extended with the regex.
 	 */
+	@SuppressWarnings("NullAway")
 	private Criteria addAppropriateLikeRegexTo(Criteria criteria, Part part, Object value) {
 
 		if (value == null) {
@@ -348,8 +381,7 @@ private Criteria addAppropriateLikeRegexTo(Criteria criteria, Part part, Object
 	 * @param part
 	 * @return the regex options or {@literal null}.
 	 */
-	@Nullable
-	private String toRegexOptions(Part part) {
+	private @Nullable String toRegexOptions(Part part) {
 
 		String regexOptions = null;
 		switch (part.shouldIgnoreCase()) {
@@ -383,19 +415,18 @@ private <T> T nextAs(Iterator<Object> iterator, Class<T> type) {
 				String.format("Expected parameter type of %s but got %s", type, parameter.getClass()));
 	}
 
-	private java.util.List<?> nextAsList(Iterator<Object> iterator, Part part) {
+	private java.util.List<?> valueAsList(Object value, Part part) {
 
-		Streamable<?> streamable = asStreamable(iterator.next());
+		Streamable<?> streamable = asStreamable(value);
 		if (!isSimpleComparisonPossible(part)) {
 
 			MatchMode matchMode = toMatchMode(part.getType());
 			String regexOptions = toRegexOptions(part);
 
 			streamable = streamable.map(it -> {
-				if (it instanceof String value) {
+				if (it instanceof String sv) {
 
-					return new BsonRegularExpression(MongoRegexCreator.INSTANCE.toRegularExpression(value, matchMode),
-							regexOptions);
+					return new BsonRegularExpression(MongoRegexCreator.INSTANCE.toRegularExpression(sv, matchMode), regexOptions);
 				}
 				return it;
 			});
@@ -414,10 +445,11 @@ private Streamable<?> asStreamable(Object value) {
 		return Streamable.of(value);
 	}
 
-	private String toLikeRegex(String source, Part part) {
+	private @Nullable String toLikeRegex(String source, Part part) {
 		return MongoRegexCreator.INSTANCE.toRegularExpression(source, toMatchMode(part.getType()));
 	}
 
+	@SuppressWarnings("NullAway")
 	private boolean isSpherical(MongoPersistentProperty property) {
 
 		if (property.isAnnotationPresent(GeoSpatialIndexed.class)) {
@@ -428,10 +460,23 @@ private boolean isSpherical(MongoPersistentProperty property) {
 		return false;
 	}
 
+	private boolean isPartOfSearchQuery(Part part) {
+		return isSearchQuery && (part.getType().equals(Type.NEAR) || part.getType().equals(Type.WITHIN));
+	}
+
+	private static void skip(Part part, Iterator<?> parameters) {
+
+		int total = part.getNumberOfArguments();
+		int i = 0;
+		while (parameters.hasNext() && i < total) {
+			parameters.next();
+			i++;
+		}
+	}
+
 	/**
 	 * Compute a {@link Type#BETWEEN} typed {@link Part} using {@link Criteria#gt(Object) $gt},
-	 * {@link Criteria#gte(Object) $gte}, {@link Criteria#lt(Object) $lt} and {@link Criteria#lte(Object) $lte}.
-	 * <br />
+	 * {@link Criteria#gte(Object) $gte}, {@link Criteria#lt(Object) $lt} and {@link Criteria#lte(Object) $lte}. <br />
 	 * In case the first {@literal value} is actually a {@link Range} the lower and upper bounds of the {@link Range} are
 	 * used according to their {@link Bound#isInclusive() inclusion} definition. Otherwise the {@literal value} is used
 	 * for {@literal $gt} and {@link Iterator#next() parameters.next()} as {@literal $lt}.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java
index dd2b78de59..c0531e0e19 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java
@@ -15,12 +15,19 @@
  */
 package org.springframework.data.mongodb.repository.query;
 
+import java.util.Collection;
+import java.util.Iterator;
 import java.util.List;
 import java.util.function.Supplier;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.Range;
+import org.springframework.data.domain.ScoringFunction;
+import org.springframework.data.domain.SearchResult;
+import org.springframework.data.domain.SearchResults;
+import org.springframework.data.domain.Similarity;
 import org.springframework.data.domain.Slice;
 import org.springframework.data.domain.SliceImpl;
 import org.springframework.data.geo.Distance;
@@ -28,18 +35,26 @@
 import org.springframework.data.geo.GeoResult;
 import org.springframework.data.geo.GeoResults;
 import org.springframework.data.geo.Point;
+import org.springframework.data.mongodb.core.ExecutableAggregationOperation.TerminatingAggregation;
 import org.springframework.data.mongodb.core.ExecutableFindOperation;
 import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery;
 import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind;
+import org.springframework.data.mongodb.core.ExecutableRemoveOperation;
+import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove;
+import org.springframework.data.mongodb.core.ExecutableRemoveOperation.TerminatingRemove;
 import org.springframework.data.mongodb.core.ExecutableUpdateOperation.ExecutableUpdate;
 import org.springframework.data.mongodb.core.MongoOperations;
+import org.springframework.data.mongodb.core.aggregation.AggregationPipeline;
+import org.springframework.data.mongodb.core.aggregation.AggregationResults;
+import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
 import org.springframework.data.mongodb.core.query.NearQuery;
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mongodb.core.query.UpdateDefinition;
+import org.springframework.data.mongodb.repository.query.VectorSearchDelegate.QueryContainer;
 import org.springframework.data.mongodb.repository.util.SliceUtils;
+import org.springframework.data.repository.query.QueryMethod;
 import org.springframework.data.support.PageableExecutionUtils;
 import org.springframework.data.util.TypeInformation;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 
@@ -55,7 +70,7 @@
  * @author Christoph Strobl
  */
 @FunctionalInterface
-interface MongoQueryExecution {
+public interface MongoQueryExecution {
 
 	@Nullable
 	Object execute(Query query);
@@ -67,12 +82,12 @@ interface MongoQueryExecution {
 	 * @author Christoph Strobl
 	 * @since 1.5
 	 */
-	final class SlicedExecution implements MongoQueryExecution {
+	final class SlicedExecution<T> implements MongoQueryExecution {
 
-		private final FindWithQuery<?> find;
+		private final FindWithQuery<T> find;
 		private final Pageable pageable;
 
-		public SlicedExecution(ExecutableFindOperation.FindWithQuery<?> find, Pageable pageable) {
+		public SlicedExecution(ExecutableFindOperation.FindWithQuery<T> find, Pageable pageable) {
 
 			Assert.notNull(find, "Find must not be null");
 			Assert.notNull(pageable, "Pageable must not be null");
@@ -83,7 +98,7 @@ public SlicedExecution(ExecutableFindOperation.FindWithQuery<?> find, Pageable p
 
 		@Override
 		@SuppressWarnings({ "unchecked", "rawtypes" })
-		public Object execute(Query query) {
+		public Slice<T> execute(Query query) {
 
 			int pageSize = pageable.getPageSize();
 
@@ -93,7 +108,7 @@ public Object execute(Query query) {
 
 			boolean hasNext = result.size() > pageSize;
 
-			return new SliceImpl<Object>(hasNext ? result.subList(0, pageSize) : result, pageable, hasNext);
+			return new SliceImpl<T>(hasNext ? result.subList(0, pageSize) : result, pageable, hasNext);
 		}
 	}
 
@@ -104,12 +119,12 @@ public Object execute(Query query) {
 	 * @author Mark Paluch
 	 * @author Christoph Strobl
 	 */
-	final class PagedExecution implements MongoQueryExecution {
+	final class PagedExecution<T> implements MongoQueryExecution {
 
-		private final FindWithQuery<?> operation;
+		private final FindWithQuery<T> operation;
 		private final Pageable pageable;
 
-		public PagedExecution(ExecutableFindOperation.FindWithQuery<?> operation, Pageable pageable) {
+		public PagedExecution(ExecutableFindOperation.FindWithQuery<T> operation, Pageable pageable) {
 
 			Assert.notNull(operation, "Operation must not be null");
 			Assert.notNull(pageable, "Pageable must not be null");
@@ -119,11 +134,11 @@ public PagedExecution(ExecutableFindOperation.FindWithQuery<?> operation, Pageab
 		}
 
 		@Override
-		public Object execute(Query query) {
+		public Page<T> execute(Query query) {
 
 			int overallLimit = query.getLimit();
 
-			TerminatingFind<?> matching = operation.matching(query);
+			TerminatingFind<T> matching = operation.matching(query);
 
 			// Apply raw pagination
 			query.with(pageable);
@@ -171,10 +186,12 @@ public Object execute(Query query) {
 			return isListOfGeoResult(method.getReturnType()) ? results.getContent() : results;
 		}
 
-		@SuppressWarnings("unchecked")
+		@SuppressWarnings({ "unchecked", "NullAway" })
 		GeoResults<Object> doExecuteQuery(Query query) {
 
 			Point nearLocation = accessor.getGeoNearLocation();
+			Assert.notNull(nearLocation, "[query.location] must not be null");
+
 			NearQuery nearQuery = NearQuery.near(nearLocation);
 
 			if (query != null) {
@@ -182,6 +199,8 @@ GeoResults<Object> doExecuteQuery(Query query) {
 			}
 
 			Range<Distance> distances = accessor.getDistanceRange();
+			Assert.notNull(nearLocation, "[query.distance] must not be null");
+
 			distances.getLowerBound().getValue().ifPresent(it -> nearQuery.minDistance(it).in(it.getMetric()));
 			distances.getUpperBound().getValue().ifPresent(it -> nearQuery.maxDistance(it).in(it.getMetric()));
 
@@ -202,6 +221,84 @@ private static boolean isListOfGeoResult(TypeInformation<?> returnType) {
 		}
 	}
 
+	/**
+	 * {@link MongoQueryExecution} to execute vector search.
+	 *
+	 * @author Mark Paluch
+	 * @author Chistoph Strobl
+	 * @since 5.0
+	 */
+	class VectorSearchExecution implements MongoQueryExecution {
+
+		private final MongoOperations operations;
+		private final TypeInformation<?> returnType;
+		private final String collectionName;
+		private final Class<?> targetType;
+		private final ScoringFunction scoringFunction;
+		private final AggregationPipeline pipeline;
+
+		VectorSearchExecution(MongoOperations operations, MongoQueryMethod method, String collectionName,
+				QueryContainer queryContainer) {
+			this(operations, queryContainer.outputType(), collectionName, method.getReturnType(), queryContainer.pipeline(),
+					queryContainer.scoringFunction());
+		}
+
+		public VectorSearchExecution(MongoOperations operations, Class<?> targetType, String collectionName,
+				TypeInformation<?> returnType, AggregationPipeline pipeline, ScoringFunction scoringFunction) {
+
+			this.operations = operations;
+			this.returnType = returnType;
+			this.collectionName = collectionName;
+			this.targetType = targetType;
+			this.scoringFunction = scoringFunction;
+			this.pipeline = pipeline;
+		}
+
+		@Override
+		@SuppressWarnings({ "unchecked", "rawtypes" })
+		public Object execute(Query query) {
+
+			TerminatingAggregation<?> executableAggregation = operations.aggregateAndReturn(targetType)
+					.inCollection(collectionName).by(TypedAggregation.newAggregation(targetType, pipeline.getOperations()));
+
+			if (!isSearchResult(returnType)) {
+				return executableAggregation.all().getMappedResults();
+			}
+
+			AggregationResults<? extends SearchResult<?>> result = executableAggregation
+					.map((raw, container) -> new SearchResult<>(container.get(),
+							Similarity.raw(raw.getDouble("__score__"), scoringFunction)))
+					.all();
+
+			return isListOfSearchResult(returnType) ? result.getMappedResults()
+					: new SearchResults(result.getMappedResults());
+		}
+
+		private static boolean isListOfSearchResult(TypeInformation<?> returnType) {
+
+			if (!Collection.class.isAssignableFrom(returnType.getType())) {
+				return false;
+			}
+
+			TypeInformation<?> componentType = returnType.getComponentType();
+			return componentType != null && SearchResult.class.equals(componentType.getType());
+		}
+
+		private static boolean isSearchResult(TypeInformation<?> returnType) {
+
+			if (SearchResults.class.isAssignableFrom(returnType.getType())) {
+				return true;
+			}
+
+			if (!Iterable.class.isAssignableFrom(returnType.getType())) {
+				return false;
+			}
+
+			TypeInformation<?> componentType = returnType.getComponentType();
+			return componentType != null && SearchResult.class.equals(componentType.getType());
+		}
+	}
+
 	/**
 	 * {@link MongoQueryExecution} to execute geo-near queries with paging.
 	 *
@@ -252,36 +349,46 @@ public Object execute(Query query) {
 	 * @author Christoph Strobl
 	 * @since 1.5
 	 */
-	final class DeleteExecution implements MongoQueryExecution {
-
-		private final MongoOperations operations;
-		private final MongoQueryMethod method;
-
-		public DeleteExecution(MongoOperations operations, MongoQueryMethod method) {
-
-			Assert.notNull(operations, "Operations must not be null");
-			Assert.notNull(method, "Method must not be null");
+	final class DeleteExecution<T> implements MongoQueryExecution {
+
+		private ExecutableRemoveOperation.ExecutableRemove<T> remove;
+		private Type type;
+
+		public DeleteExecution(ExecutableRemove<T> remove, QueryMethod queryMethod) {
+			this.remove = remove;
+			if (queryMethod.isCollectionQuery()) {
+				this.type = Type.FIND_AND_REMOVE_ALL;
+			} else if (queryMethod.isQueryForEntity()
+					&& !ClassUtils.isPrimitiveOrWrapper(queryMethod.getReturnedObjectType())) {
+				this.type = Type.FIND_AND_REMOVE_ONE;
+			} else {
+				this.type = Type.ALL;
+			}
+		}
 
-			this.operations = operations;
-			this.method = method;
+		public DeleteExecution(ExecutableRemove<T> remove, Type type) {
+			this.remove = remove;
+			this.type = type;
 		}
 
 		@Override
-		public Object execute(Query query) {
-
-			String collectionName = method.getEntityInformation().getCollectionName();
-			Class<?> type = method.getEntityInformation().getJavaType();
-
-			if (method.isCollectionQuery()) {
-				return operations.findAllAndRemove(query, type, collectionName);
-			}
-
-			if (method.isQueryForEntity() && !ClassUtils.isPrimitiveOrWrapper(method.getReturnedObjectType())) {
-				return operations.findAndRemove(query, type, collectionName);
+		public @Nullable Object execute(Query query) {
+
+			TerminatingRemove<T> doRemove = remove.matching(query);
+			if (Type.ALL.equals(type)) {
+				DeleteResult result = doRemove.all();
+				return result.wasAcknowledged() ? Long.valueOf(result.getDeletedCount()) : Long.valueOf(0);
+			} else if (Type.FIND_AND_REMOVE_ALL.equals(type)) {
+				return doRemove.findAndRemove();
+			} else if (Type.FIND_AND_REMOVE_ONE.equals(type)) {
+				Iterator<T> removed = doRemove.findAndRemove().iterator();
+				return removed.hasNext() ? removed.next() : null;
 			}
+			throw new RuntimeException();
+		}
 
-			DeleteResult writeResult = operations.remove(query, type, collectionName);
-			return writeResult.wasAcknowledged() ? writeResult.getDeletedCount() : 0L;
+		public enum Type {
+			FIND_AND_REMOVE_ONE, FIND_AND_REMOVE_ALL, ALL
 		}
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java
index d3fe22b4ef..de628d59f4 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java
@@ -21,6 +21,7 @@
 import java.util.Optional;
 import java.util.function.Function;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.core.annotation.AnnotatedElementUtils;
 import org.springframework.data.mapping.context.MappingContext;
 import org.springframework.data.mongodb.core.annotation.Collation;
@@ -34,6 +35,7 @@
 import org.springframework.data.mongodb.repository.ReadPreference;
 import org.springframework.data.mongodb.repository.Tailable;
 import org.springframework.data.mongodb.repository.Update;
+import org.springframework.data.mongodb.repository.VectorSearch;
 import org.springframework.data.mongodb.util.BsonUtils;
 import org.springframework.data.projection.ProjectionFactory;
 import org.springframework.data.repository.core.RepositoryMetadata;
@@ -43,7 +45,6 @@
 import org.springframework.data.util.ReactiveWrappers;
 import org.springframework.data.util.ReflectionUtils;
 import org.springframework.data.util.TypeInformation;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.ConcurrentReferenceHashMap;
@@ -117,7 +118,7 @@ public boolean hasAnnotatedQuery() {
 	 * @return
 	 */
 	@Nullable
-	String getAnnotatedQuery() {
+	public String getAnnotatedQuery() {
 		return findAnnotatedQuery().orElse(null);
 	}
 
@@ -191,7 +192,7 @@ public boolean isGeoNearQuery() {
 	}
 
 	/**
-	 * Returns the {@link Query} annotation that is applied to the method or {@code null} if none available.
+	 * Returns the {@link Query} annotation that is applied to the method or {@literal null} if none available.
 	 *
 	 * @return
 	 */
@@ -204,7 +205,7 @@ Optional<Query> lookupQueryAnnotation() {
 		return doFindAnnotation(Query.class);
 	}
 
-	TypeInformation<?> getReturnType() {
+	public TypeInformation<?> getReturnType() {
 		return TypeInformation.fromReturnTypeOf(method);
 	}
 
@@ -217,7 +218,7 @@ public boolean hasQueryMetaAttributes() {
 	}
 
 	/**
-	 * Returns the {@link Meta} annotation that is applied to the method or {@code null} if not available.
+	 * Returns the {@link Meta} annotation that is applied to the method or {@literal null} if not available.
 	 *
 	 * @return
 	 * @since 1.6
@@ -228,7 +229,7 @@ Meta getMetaAnnotation() {
 	}
 
 	/**
-	 * Returns the {@link Tailable} annotation that is applied to the method or {@code null} if not available.
+	 * Returns the {@link Tailable} annotation that is applied to the method or {@literal null} if not available.
 	 *
 	 * @return
 	 * @since 2.0
@@ -414,10 +415,28 @@ private Optional<String[]> findAnnotatedAggregation() {
 				.filter(it -> !ObjectUtils.isEmpty(it));
 	}
 
+	/**
+	 * Returns whether the method has an annotated vector search.
+	 *
+	 * @return true if {@link VectorSearch} is present.
+	 * @since 5.0
+	 */
+	public boolean hasAnnotatedVectorSearch() {
+		return findAnnotatedVectorSearch().isPresent();
+	}
+
+	Optional<VectorSearch> findAnnotatedVectorSearch() {
+		return lookupVectorSearchAnnotation();
+	}
+
 	Optional<Aggregation> lookupAggregationAnnotation() {
 		return doFindAnnotation(Aggregation.class);
 	}
 
+	Optional<VectorSearch> lookupVectorSearchAnnotation() {
+		return doFindAnnotation(VectorSearch.class);
+	}
+
 	Optional<Update> lookupUpdateAnnotation() {
 		return doFindAnnotation(Update.class);
 	}
@@ -461,7 +480,7 @@ public boolean hasAnnotatedUpdate() {
 	 * @return the {@link Update} or {@literal null} if not present.
 	 * @since 3.4
 	 */
-	public Update getUpdateSource() {
+	public @Nullable Update getUpdateSource() {
 		return lookupUpdateAnnotation().orElse(null);
 	}
 
@@ -471,6 +490,7 @@ public Update getUpdateSource() {
 	 * @since 3.4
 	 * @throws IllegalStateException
 	 */
+	@SuppressWarnings("NullAway")
 	public void verify() {
 
 		if (isModifyingQuery()) {
@@ -509,6 +529,7 @@ public void verify() {
 		}
 	}
 
+	@SuppressWarnings("NullAway")
 	private boolean isNumericOrVoidReturnValue() {
 
 		Class<?> resultType = getReturnedObjectType();
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java
index afabf9c37e..9682e4971f 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java
@@ -18,8 +18,6 @@
 import org.bson.Document;
 import org.bson.json.JsonParseException;
 
-import org.springframework.core.env.StandardEnvironment;
-import org.springframework.data.expression.ValueExpressionParser;
 import org.springframework.data.mapping.context.MappingContext;
 import org.springframework.data.mongodb.core.MongoOperations;
 import org.springframework.data.mongodb.core.MongoTemplate;
@@ -29,14 +27,11 @@
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mongodb.core.query.TextCriteria;
 import org.springframework.data.repository.query.QueryMethod;
-import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
-import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor;
 import org.springframework.data.repository.query.RepositoryQuery;
 import org.springframework.data.repository.query.ResultProcessor;
 import org.springframework.data.repository.query.ReturnedType;
 import org.springframework.data.repository.query.ValueExpressionDelegate;
 import org.springframework.data.repository.query.parser.PartTree;
-import org.springframework.expression.ExpressionParser;
 import org.springframework.util.StringUtils;
 
 /**
@@ -54,26 +49,6 @@ public class PartTreeMongoQuery extends AbstractMongoQuery {
 	private final MappingContext<?, MongoPersistentProperty> context;
 	private final ResultProcessor processor;
 
-	/**
-	 * Creates a new {@link PartTreeMongoQuery} from the given {@link QueryMethod} and {@link MongoTemplate}.
-	 *
-	 * @param method must not be {@literal null}.
-	 * @param mongoOperations must not be {@literal null}.
-	 * @param expressionParser must not be {@literal null}.
-	 * @param evaluationContextProvider must not be {@literal null}.
-	 * @deprecated since 4.4, use the constructors accepting {@link QueryMethodValueEvaluationContextAccessor} instead.
-	 */
-	@Deprecated(since = "4.4.0")
-	public PartTreeMongoQuery(MongoQueryMethod method, MongoOperations mongoOperations, ExpressionParser expressionParser,
-			QueryMethodEvaluationContextProvider evaluationContextProvider) {
-		super(method, mongoOperations, expressionParser, evaluationContextProvider);
-
-		this.processor = method.getResultProcessor();
-		this.tree = new PartTree(method.getName(), processor.getReturnedType().getDomainType());
-		this.isGeoNearQuery = method.isGeoNearQuery();
-		this.context = mongoOperations.getConverter().getMappingContext();
-	}
-
 	/**
 	 * Creates a new {@link PartTreeMongoQuery} from the given {@link QueryMethod} and {@link MongoTemplate}.
 	 *
@@ -103,9 +78,10 @@ public PartTree getTree() {
 	}
 
 	@Override
+	@SuppressWarnings("NullAway")
 	protected Query createQuery(ConvertingParameterAccessor accessor) {
 
-		MongoQueryCreator creator = new MongoQueryCreator(tree, accessor, context, isGeoNearQuery);
+		MongoQueryCreator creator = new MongoQueryCreator(tree, accessor, context, isGeoNearQuery, false);
 		Query query = creator.createQuery();
 
 		if (tree.isLimiting()) {
@@ -150,7 +126,7 @@ protected Query createQuery(ConvertingParameterAccessor accessor) {
 
 	@Override
 	protected Query createCountQuery(ConvertingParameterAccessor accessor) {
-		return new MongoQueryCreator(tree, accessor, context, false).createQuery();
+		return new MongoQueryCreator(tree, accessor, context, false, false).createQuery();
 	}
 
 	@Override
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java
index 431510f11b..4b7262749a 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java
@@ -21,13 +21,12 @@
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.bson.Document;
-
+import org.jspecify.annotations.Nullable;
 import org.springframework.aop.framework.ProxyFactory;
 import org.springframework.data.mongodb.core.query.BasicQuery;
 import org.springframework.data.mongodb.core.query.Collation;
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mapping.model.ValueExpressionEvaluator;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ClassUtils;
 
 /**
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoParameterAccessor.java
index 324f01d61f..9534a9cf4f 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoParameterAccessor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoParameterAccessor.java
@@ -15,6 +15,7 @@
  */
 package org.springframework.data.mongodb.repository.query;
 
+import org.jspecify.annotations.Nullable;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
@@ -51,16 +52,21 @@ public ReactiveMongoParameterAccessor(MongoQueryMethod method, Object[] values)
 	 * @see org.springframework.data.mongodb.repository.query.MongoParametersParameterAccessor#getValues()
 	 */
 	@Override
-	public Object[] getValues() {
+	public Object @Nullable[] getValues() {
 
-		Object[] result = new Object[super.getValues().length];
+		Object[] values = super.getValues();
+		if(values == null) {
+			return new Object[0];
+		}
+
+		Object[] result = new Object[values.length];
 		for (int i = 0; i < result.length; i++) {
 			result[i] = getValue(i);
 		}
 		return result;
 	}
 
-	public Object getBindableValue(int index) {
+	public @Nullable Object getBindableValue(int index) {
 		return getValue(getParameters().getBindableParameter(index).getIndex());
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java
index d18c6a989c..29e2127e18 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java
@@ -18,26 +18,32 @@
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
+import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.reactivestreams.Publisher;
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.data.convert.DtoInstantiatingConverter;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.Range;
+import org.springframework.data.domain.SearchResult;
+import org.springframework.data.domain.Similarity;
 import org.springframework.data.geo.Distance;
 import org.springframework.data.geo.GeoResult;
 import org.springframework.data.geo.Point;
 import org.springframework.data.mapping.model.EntityInstantiators;
 import org.springframework.data.mongodb.core.ReactiveMongoOperations;
 import org.springframework.data.mongodb.core.ReactiveUpdateOperation.ReactiveUpdate;
+import org.springframework.data.mongodb.core.aggregation.AggregationPipeline;
+import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
 import org.springframework.data.mongodb.core.query.NearQuery;
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mongodb.core.query.UpdateDefinition;
+import org.springframework.data.mongodb.repository.query.VectorSearchDelegate.QueryContainer;
 import org.springframework.data.repository.query.ResultProcessor;
 import org.springframework.data.repository.query.ReturnedType;
 import org.springframework.data.util.ReactiveWrappers;
 import org.springframework.data.util.ReflectionUtils;
 import org.springframework.data.util.TypeInformation;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 
@@ -86,6 +92,8 @@ public Publisher<? extends Object> execute(Query query, Class<?> type, String co
 		private Flux<GeoResult<Object>> doExecuteQuery(@Nullable Query query, Class<?> type, String collection) {
 
 			Point nearLocation = accessor.getGeoNearLocation();
+			Assert.notNull(nearLocation, "[query.location] ist not present");
+
 			NearQuery nearQuery = NearQuery.near(nearLocation);
 
 			if (query != null) {
@@ -93,6 +101,8 @@ private Flux<GeoResult<Object>> doExecuteQuery(@Nullable Query query, Class<?> t
 			}
 
 			Range<Distance> distances = accessor.getDistanceRange();
+
+			Assert.notNull(distances, "[query.range] ist not present");
 			distances.getUpperBound().getValue().ifPresent(it -> nearQuery.maxDistance(it).in(it.getMetric()));
 			distances.getLowerBound().getValue().ifPresent(it -> nearQuery.minDistance(it).in(it.getMetric()));
 
@@ -113,6 +123,57 @@ private boolean isStreamOfGeoResult() {
 		}
 	}
 
+	/**
+	 * {@link ReactiveMongoQueryExecution} to execute vector search.
+	 *
+	 * @author Mark Paluch
+	 * @since 5.0
+	 */
+	class VectorSearchExecution implements ReactiveMongoQueryExecution {
+
+		private final ReactiveMongoOperations operations;
+		private final QueryContainer queryMetadata;
+		private final AggregationPipeline pipeline;
+		private final boolean returnSearchResult;
+
+		VectorSearchExecution(ReactiveMongoOperations operations, MongoQueryMethod method, QueryContainer queryMetadata) {
+
+			this.operations = operations;
+			this.queryMetadata = queryMetadata;
+			this.pipeline = queryMetadata.pipeline();
+			this.returnSearchResult = isSearchResult(method.getReturnType());
+		}
+
+		@Override
+		public Publisher<? extends Object> execute(Query query, Class<?> type, String collection) {
+
+			Flux<Document> aggregate = operations.aggregate(
+					TypedAggregation.newAggregation(queryMetadata.outputType(), pipeline.getOperations()), collection,
+					Document.class);
+
+			return aggregate.map(document -> {
+
+				Object mappedResult = operations.getConverter().read(queryMetadata.outputType(), document);
+
+				return returnSearchResult
+						? new SearchResult<>(mappedResult,
+								Similarity.raw(document.getDouble(queryMetadata.scoreField()), queryMetadata.scoringFunction()))
+						: mappedResult;
+			});
+		}
+
+		private static boolean isSearchResult(TypeInformation<?> returnType) {
+
+			if (!Publisher.class.isAssignableFrom(returnType.getType())) {
+				return false;
+			}
+
+			TypeInformation<?> componentType = returnType.getComponentType();
+			return componentType != null && SearchResult.class.equals(componentType.getType());
+		}
+
+	}
+
 	/**
 	 * {@link ReactiveMongoQueryExecution} removing documents matching the query.
 	 *
@@ -195,6 +256,7 @@ public ResultProcessingExecution(ReactiveMongoQueryExecution delegate, Converter
 		}
 
 		@Override
+		@SuppressWarnings("NullAway")
 		public Publisher<? extends Object> execute(Query query, Class<?> type, String collection) {
 			return (Publisher) converter.convert(delegate.execute(query, type, collection));
 		}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java
index 5787cca5a5..9a17b2b5fc 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java
@@ -28,14 +28,11 @@
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mongodb.core.query.TextCriteria;
 import org.springframework.data.repository.query.QueryMethod;
-import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor;
-import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
 import org.springframework.data.repository.query.RepositoryQuery;
 import org.springframework.data.repository.query.ResultProcessor;
 import org.springframework.data.repository.query.ReturnedType;
 import org.springframework.data.repository.query.ValueExpressionDelegate;
 import org.springframework.data.repository.query.parser.PartTree;
-import org.springframework.expression.ExpressionParser;
 import org.springframework.util.StringUtils;
 
 /**
@@ -52,26 +49,6 @@ public class ReactivePartTreeMongoQuery extends AbstractReactiveMongoQuery {
 	private final MappingContext<?, MongoPersistentProperty> context;
 	private final ResultProcessor processor;
 
-	/**
-	 * Creates a new {@link ReactivePartTreeMongoQuery} from the given {@link QueryMethod} and {@link MongoTemplate}.
-	 *
-	 * @param method must not be {@literal null}.
-	 * @param mongoOperations must not be {@literal null}.
-	 * @param expressionParser must not be {@literal null}.
-	 * @param evaluationContextProvider must not be {@literal null}.
-	 * @deprecated since 4.4.0, use the constructors accepting {@link QueryMethodValueEvaluationContextAccessor} instead.
-	 */
-	@Deprecated(since = "4.4.0")
-	public ReactivePartTreeMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations mongoOperations,
-			ExpressionParser expressionParser, ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) {
-		super(method, mongoOperations, expressionParser, evaluationContextProvider);
-
-		this.processor = method.getResultProcessor();
-		this.tree = new PartTree(method.getName(), processor.getReturnedType().getDomainType());
-		this.isGeoNearQuery = method.isGeoNearQuery();
-		this.context = mongoOperations.getConverter().getMappingContext();
-	}
-
 	/**
 	 * Creates a new {@link ReactivePartTreeMongoQuery} from the given {@link QueryMethod} and {@link MongoTemplate}.
 	 *
@@ -110,9 +87,10 @@ protected Mono<Query> createCountQuery(ConvertingParameterAccessor accessor) {
 		return Mono.fromSupplier(() -> createQueryInternal(accessor, true));
 	}
 
+	@SuppressWarnings("NullAway")
 	private Query createQueryInternal(ConvertingParameterAccessor accessor, boolean isCountQuery) {
 
-		MongoQueryCreator creator = new MongoQueryCreator(tree, accessor, context, !isCountQuery && isGeoNearQuery);
+		MongoQueryCreator creator = new MongoQueryCreator(tree, accessor, context, !isCountQuery && isGeoNearQuery, false);
 		Query query = creator.createQuery();
 
 		if (isCountQuery) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java
index ff01d8f8a3..ebc33cef96 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java
@@ -21,6 +21,7 @@
 import java.util.List;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.reactivestreams.Publisher;
 
 import org.springframework.data.mongodb.core.ReactiveMongoOperations;
@@ -29,12 +30,9 @@
 import org.springframework.data.mongodb.core.convert.MongoConverter;
 import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
 import org.springframework.data.mongodb.core.query.Query;
-import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
 import org.springframework.data.repository.query.ResultProcessor;
 import org.springframework.data.repository.query.ValueExpressionDelegate;
 import org.springframework.data.util.ReflectionUtils;
-import org.springframework.expression.ExpressionParser;
-import org.springframework.lang.Nullable;
 
 /**
  * A reactive {@link org.springframework.data.repository.query.RepositoryQuery} to use a plain JSON String to create an
@@ -49,24 +47,6 @@ public class ReactiveStringBasedAggregation extends AbstractReactiveMongoQuery {
 	private final ReactiveMongoOperations reactiveMongoOperations;
 	private final MongoConverter mongoConverter;
 
-	/**
-	 * @param method must not be {@literal null}.
-	 * @param reactiveMongoOperations must not be {@literal null}.
-	 * @param expressionParser must not be {@literal null}.
-	 * @param evaluationContextProvider must not be {@literal null}.
-	 * @deprecated since 4.4.0, use the constructors accepting {@link ValueExpressionDelegate} instead.
-	 */
-	@Deprecated(since = "4.4.0")
-	public ReactiveStringBasedAggregation(ReactiveMongoQueryMethod method,
-			ReactiveMongoOperations reactiveMongoOperations, ExpressionParser expressionParser,
-			ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) {
-
-		super(method, reactiveMongoOperations, expressionParser, evaluationContextProvider);
-
-		this.reactiveMongoOperations = reactiveMongoOperations;
-		this.mongoConverter = reactiveMongoOperations.getConverter();
-	}
-
 	/**
 	 * @param method must not be {@literal null}.
 	 * @param reactiveMongoOperations must not be {@literal null}.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java
index 0e980fcfaf..4bfe2ca39f 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java
@@ -15,12 +15,14 @@
  */
 package org.springframework.data.mongodb.repository.query;
 
+import org.jspecify.annotations.Nullable;
+import org.springframework.lang.Contract;
 import reactor.core.publisher.Mono;
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.bson.Document;
-
+import org.jspecify.annotations.NonNull;
 import org.springframework.data.expression.ValueExpressionParser;
 import org.springframework.data.mongodb.core.MongoOperations;
 import org.springframework.data.mongodb.core.ReactiveMongoOperations;
@@ -28,13 +30,8 @@
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mongodb.util.json.ParameterBindingContext;
 import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec;
-import org.springframework.data.repository.query.ReactiveExtensionAwareQueryMethodEvaluationContextProvider;
-import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
 import org.springframework.data.repository.query.ValueExpressionDelegate;
 import org.springframework.data.spel.ExpressionDependencies;
-import org.springframework.expression.ExpressionParser;
-import org.springframework.expression.spel.standard.SpelExpressionParser;
-import org.springframework.lang.NonNull;
 import org.springframework.util.Assert;
 
 /**
@@ -50,7 +47,7 @@ public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery {
 	private static final Log LOG = LogFactory.getLog(ReactiveStringBasedMongoQuery.class);
 
 	private final String query;
-	private final String fieldSpec;
+	private final @Nullable String fieldSpec;
 
 	private final ValueExpressionParser expressionParser;
 
@@ -59,73 +56,15 @@ public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery {
 	private final boolean isDeleteQuery;
 
 	/**
-	 * Creates a new {@link ReactiveStringBasedMongoQuery} for the given {@link MongoQueryMethod} and
-	 * {@link MongoOperations}.
-	 *
-	 * @param method must not be {@literal null}.
-	 * @param mongoOperations must not be {@literal null}.
-	 * @param expressionParser must not be {@literal null}.
-	 * @param evaluationContextProvider must not be {@literal null}.
-	 * @deprecated since 4.4.0, use the constructors accepting {@link ValueExpressionDelegate} instead.
-	 */
-	@Deprecated(since = "4.4.0")
-	public ReactiveStringBasedMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations mongoOperations,
-			ExpressionParser expressionParser, ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) {
-		this(method.getAnnotatedQuery(), method, mongoOperations, expressionParser, evaluationContextProvider);
-	}
-
-	/**
-	 * Creates a new {@link ReactiveStringBasedMongoQuery} for the given {@link String}, {@link MongoQueryMethod},
-	 * {@link MongoOperations}, {@link SpelExpressionParser} and
-	 * {@link ReactiveExtensionAwareQueryMethodEvaluationContextProvider}.
-	 *
-	 * @param query must not be {@literal null}.
-	 * @param method must not be {@literal null}.
-	 * @param mongoOperations must not be {@literal null}.
-	 * @param expressionParser must not be {@literal null}.
-	 * @deprecated since 4.4.0, use the constructors accepting {@link ValueExpressionDelegate} instead.
-	 */
-	@Deprecated(since = "4.4.0")
-	public ReactiveStringBasedMongoQuery(String query, ReactiveMongoQueryMethod method,
-			ReactiveMongoOperations mongoOperations, ExpressionParser expressionParser,
-			ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) {
-		super(method, mongoOperations, expressionParser, evaluationContextProvider);
-
-		Assert.notNull(query, "Query must not be null");
-
-		this.query = query;
-		this.expressionParser = ValueExpressionParser.create(() -> expressionParser);
-		this.fieldSpec = method.getFieldSpecification();
-
-		if (method.hasAnnotatedQuery()) {
-
-			org.springframework.data.mongodb.repository.Query queryAnnotation = method.getQueryAnnotation();
-
-			this.isCountQuery = queryAnnotation.count();
-			this.isExistsQuery = queryAnnotation.exists();
-			this.isDeleteQuery = queryAnnotation.delete();
-
-			if (hasAmbiguousProjectionFlags(this.isCountQuery, this.isExistsQuery, this.isDeleteQuery)) {
-				throw new IllegalArgumentException(String.format(COUNT_EXISTS_AND_DELETE, method));
-			}
-
-		} else {
-
-			this.isCountQuery = false;
-			this.isExistsQuery = false;
-			this.isDeleteQuery = false;
-		}
-	}
-
-	/**
-	 * Creates a new {@link ReactiveStringBasedMongoQuery} for the given {@link MongoQueryMethod},
-	 * {@link MongoOperations} and {@link ValueExpressionDelegate}.
+	 * Creates a new {@link ReactiveStringBasedMongoQuery} for the given {@link MongoQueryMethod}, {@link MongoOperations}
+	 * and {@link ValueExpressionDelegate}.
 	 *
 	 * @param method must not be {@literal null}.
 	 * @param mongoOperations must not be {@literal null}.
 	 * @param delegate must not be {@literal null}.
 	 * @since 4.4.0
 	 */
+	@SuppressWarnings("NullAway")
 	public ReactiveStringBasedMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations mongoOperations,
 			ValueExpressionDelegate delegate) {
 		this(method.getAnnotatedQuery(), method, mongoOperations, delegate);
@@ -141,7 +80,8 @@ public ReactiveStringBasedMongoQuery(ReactiveMongoQueryMethod method, ReactiveMo
 	 * @param delegate must not be {@literal null}.
 	 * @since 4.4.0
 	 */
-	public ReactiveStringBasedMongoQuery(@NonNull String query, ReactiveMongoQueryMethod method,
+	@SuppressWarnings("NullAway")
+	public ReactiveStringBasedMongoQuery(String query, ReactiveMongoQueryMethod method,
 			ReactiveMongoOperations mongoOperations, ValueExpressionDelegate delegate) {
 
 		super(method, mongoOperations, delegate);
@@ -195,7 +135,7 @@ protected Mono<Query> createQuery(ConvertingParameterAccessor accessor) {
 		});
 	}
 
-	private Mono<ParameterBindingContext> getBindingContext(String json, ConvertingParameterAccessor accessor,
+	private Mono<ParameterBindingContext> getBindingContext(@Nullable String json, ConvertingParameterAccessor accessor,
 			ParameterBindingDocumentCodec codec) {
 
 		ExpressionDependencies dependencies = codec.captureExpressionDependencies(json, accessor::getBindableValue,
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveVectorSearchAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveVectorSearchAggregation.java
new file mode 100644
index 0000000000..cf75c7db94
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveVectorSearchAggregation.java
@@ -0,0 +1,123 @@
+/*
+ * 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.mongodb.repository.query;
+
+import reactor.core.publisher.Mono;
+
+import org.bson.Document;
+import org.reactivestreams.Publisher;
+import org.springframework.data.mongodb.InvalidMongoDbApiUsageException;
+import org.springframework.data.mongodb.core.MongoOperations;
+import org.springframework.data.mongodb.core.ReactiveMongoOperations;
+import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.mongodb.repository.VectorSearch;
+import org.springframework.data.mongodb.repository.query.VectorSearchDelegate.QueryContainer;
+import org.springframework.data.mongodb.util.json.ParameterBindingContext;
+import org.springframework.data.repository.query.ResultProcessor;
+import org.springframework.data.repository.query.ValueExpressionDelegate;
+import org.springframework.data.spel.ExpressionDependencies;
+
+/**
+ * {@link AbstractReactiveMongoQuery} implementation to run a {@link VectorSearchAggregation}. The pre-filter is either
+ * derived from the method name or provided through {@link VectorSearch#filter()}.
+ *
+ * @author Mark Paluch
+ * @since 5.0
+ */
+public class ReactiveVectorSearchAggregation extends AbstractReactiveMongoQuery {
+
+	private final ReactiveMongoOperations mongoOperations;
+	private final MongoPersistentEntity<?> collectionEntity;
+	private final ValueExpressionDelegate valueExpressionDelegate;
+	private final VectorSearchDelegate delegate;
+
+	/**
+	 * Creates a new {@link ReactiveVectorSearchAggregation} from the given {@link MongoQueryMethod} and
+	 * {@link MongoOperations}.
+	 *
+	 * @param method must not be {@literal null}.
+	 * @param mongoOperations must not be {@literal null}.
+	 * @param delegate must not be {@literal null}.
+	 */
+	public ReactiveVectorSearchAggregation(ReactiveMongoQueryMethod method, ReactiveMongoOperations mongoOperations,
+			ValueExpressionDelegate delegate) {
+
+		super(method, mongoOperations, delegate);
+
+		this.valueExpressionDelegate = delegate;
+		if (!method.isSearchQuery() && !method.isCollectionQuery()) {
+			throw new InvalidMongoDbApiUsageException(String.format(
+					"Repository Vector Search method '%s' must return either return SearchResults<T> or List<T> but was %s",
+					method.getName(), method.getReturnType().getType().getSimpleName()));
+		}
+
+		this.mongoOperations = mongoOperations;
+		this.collectionEntity = method.getEntityInformation().getCollectionEntity();
+		this.delegate = new VectorSearchDelegate(method, mongoOperations.getConverter(), delegate);
+	}
+
+	@Override
+	protected Publisher<Object> doExecute(ReactiveMongoQueryMethod method, ResultProcessor processor,
+			ConvertingParameterAccessor accessor, @org.jspecify.annotations.Nullable Class<?> typeToRead) {
+
+		return getParameterBindingCodec().flatMapMany(codec -> {
+
+			String json = delegate.getQueryString();
+			ExpressionDependencies dependencies = codec.captureExpressionDependencies(json, accessor::getBindableValue,
+					valueExpressionDelegate);
+
+			return getValueExpressionEvaluatorLater(dependencies, accessor).flatMapMany(expressionEvaluator -> {
+
+				ParameterBindingContext bindingContext = new ParameterBindingContext(accessor::getBindableValue,
+						expressionEvaluator);
+				QueryContainer query = delegate.createQuery(expressionEvaluator, processor, accessor, typeToRead, codec,
+						bindingContext);
+
+				ReactiveMongoQueryExecution.VectorSearchExecution execution = new ReactiveMongoQueryExecution.VectorSearchExecution(
+						mongoOperations, method, query);
+
+				return execution.execute(query.query(), Document.class, collectionEntity.getCollection());
+			});
+		});
+	}
+
+	@Override
+	protected Mono<Query> createQuery(ConvertingParameterAccessor accessor) {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	protected boolean isCountQuery() {
+		return false;
+	}
+
+	@Override
+	protected boolean isExistsQuery() {
+		return false;
+	}
+
+	@Override
+	protected boolean isDeleteQuery() {
+		return false;
+	}
+
+	@Override
+	protected boolean isLimiting() {
+		return false;
+	}
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringAggregationOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringAggregationOperation.java
index 724c8f29ef..289b953b27 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringAggregationOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringAggregationOperation.java
@@ -20,9 +20,9 @@
 import java.util.regex.Pattern;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
 import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext;
-import org.springframework.lang.Nullable;
 
 /**
  * String-based aggregation operation for a repository query method.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java
index 7ad5d78fa6..3f6a48e84c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java
@@ -20,7 +20,7 @@
 import java.util.stream.Stream;
 
 import org.bson.Document;
-
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.SliceImpl;
 import org.springframework.data.mongodb.InvalidMongoDbApiUsageException;
@@ -29,13 +29,9 @@
 import org.springframework.data.mongodb.core.convert.MongoConverter;
 import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
 import org.springframework.data.mongodb.core.query.Query;
-import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
-import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor;
 import org.springframework.data.repository.query.ResultProcessor;
 import org.springframework.data.repository.query.ValueExpressionDelegate;
 import org.springframework.data.util.ReflectionUtils;
-import org.springframework.expression.ExpressionParser;
-import org.springframework.lang.Nullable;
 
 /**
  * {@link AbstractMongoQuery} implementation to run string-based aggregations using
@@ -51,30 +47,6 @@ public class StringBasedAggregation extends AbstractMongoQuery {
 	private final MongoOperations mongoOperations;
 	private final MongoConverter mongoConverter;
 
-	/**
-	 * Creates a new {@link StringBasedAggregation} from the given {@link MongoQueryMethod} and {@link MongoOperations}.
-	 *
-	 * @param method must not be {@literal null}.
-	 * @param mongoOperations must not be {@literal null}.
-	 * @param expressionParser must not be {@literal null}.
-	 * @param evaluationContextProvider must not be {@literal null}.
-	 * @deprecated since 4.4.0, use the constructors accepting {@link QueryMethodValueEvaluationContextAccessor} instead.
-	 */
-	@Deprecated(since = "4.4.0")
-	public StringBasedAggregation(MongoQueryMethod method, MongoOperations mongoOperations,
-			ExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) {
-		super(method, mongoOperations, expressionParser, evaluationContextProvider);
-
-		if (method.isPageQuery()) {
-			throw new InvalidMongoDbApiUsageException(String.format(
-					"Repository aggregation method '%s' does not support '%s' return type; Please use 'Slice' or 'List' instead",
-					method.getName(), method.getReturnType().getType().getSimpleName()));
-		}
-
-		this.mongoOperations = mongoOperations;
-		this.mongoConverter = mongoOperations.getConverter();
-	}
-
 	/**
 	 * Creates a new {@link StringBasedAggregation} from the given {@link MongoQueryMethod} and {@link MongoOperations}.
 	 *
@@ -99,8 +71,7 @@ public StringBasedAggregation(MongoQueryMethod method, MongoOperations mongoOper
 
 	@SuppressWarnings("unchecked")
 	@Override
-	@Nullable
-	protected Object doExecute(MongoQueryMethod method, ResultProcessor processor, ConvertingParameterAccessor accessor,
+	protected @Nullable Object doExecute(MongoQueryMethod method, ResultProcessor processor, ConvertingParameterAccessor accessor,
 			@Nullable Class<?> ignore) {
 
 		return AggregationUtils.doAggregate(AggregationUtils.computePipeline(this, method, accessor), method, processor,
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java
index abc158f88a..c990d3269d 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java
@@ -22,11 +22,8 @@
 import org.springframework.data.mongodb.core.MongoOperations;
 import org.springframework.data.mongodb.core.query.BasicQuery;
 import org.springframework.data.mongodb.core.query.Query;
-import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
 import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor;
 import org.springframework.data.repository.query.ValueExpressionDelegate;
-import org.springframework.expression.ExpressionParser;
-import org.springframework.expression.spel.standard.SpelExpressionParser;
 import org.springframework.util.Assert;
 
 /**
@@ -49,47 +46,6 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
 	private final boolean isExistsQuery;
 	private final boolean isDeleteQuery;
 
-	/**
-	 * Creates a new {@link StringBasedMongoQuery} for the given {@link MongoQueryMethod}, {@link MongoOperations},
-	 * {@link SpelExpressionParser} and {@link QueryMethodEvaluationContextProvider}.
-	 *
-	 * @param method must not be {@literal null}.
-	 * @param mongoOperations must not be {@literal null}.
-	 * @param expressionParser must not be {@literal null}.
-	 * @param evaluationContextProvider must not be {@literal null}.
-	 * @deprecated since 4.4.0, use the constructors accepting {@link ValueExpressionDelegate} instead.
-	 */
-	@Deprecated(since = "4.4.0")
-	public StringBasedMongoQuery(MongoQueryMethod method, MongoOperations mongoOperations,
-			ExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) {
-		super(method, mongoOperations, expressionParser, evaluationContextProvider);
-
-		String query = method.getAnnotatedQuery();
-		Assert.notNull(query, "Query must not be null");
-
-		this.query = query;
-		this.fieldSpec = method.getFieldSpecification();
-
-		if (method.hasAnnotatedQuery()) {
-
-			org.springframework.data.mongodb.repository.Query queryAnnotation = method.getQueryAnnotation();
-
-			this.isCountQuery = queryAnnotation.count();
-			this.isExistsQuery = queryAnnotation.exists();
-			this.isDeleteQuery = queryAnnotation.delete();
-
-			if (hasAmbiguousProjectionFlags(this.isCountQuery, this.isExistsQuery, this.isDeleteQuery)) {
-				throw new IllegalArgumentException(String.format(COUNT_EXISTS_AND_DELETE, method));
-			}
-
-		} else {
-
-			this.isCountQuery = false;
-			this.isExistsQuery = false;
-			this.isDeleteQuery = false;
-		}
-	}
-
 	/**
 	 * Creates a new {@link StringBasedMongoQuery} for the given {@link MongoQueryMethod}, {@link MongoOperations},
 	 * {@link ValueExpressionDelegate}.
@@ -99,6 +55,7 @@ public StringBasedMongoQuery(MongoQueryMethod method, MongoOperations mongoOpera
 	 * @param expressionSupport must not be {@literal null}.
 	 * @since 4.4.0
 	 */
+	@SuppressWarnings("NullAway")
 	public StringBasedMongoQuery(MongoQueryMethod method, MongoOperations mongoOperations,
 			ValueExpressionDelegate expressionSupport) {
 		this(method.getAnnotatedQuery(), method, mongoOperations, expressionSupport);
@@ -114,6 +71,7 @@ public StringBasedMongoQuery(MongoQueryMethod method, MongoOperations mongoOpera
 	 * @param expressionSupport must not be {@literal null}.
 	 * @since 4.3
 	 */
+	@SuppressWarnings("NullAway")
 	public StringBasedMongoQuery(String query, MongoQueryMethod method, MongoOperations mongoOperations,
 			ValueExpressionDelegate expressionSupport) {
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ValueExpressionDelegateValueExpressionEvaluator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ValueExpressionDelegateValueExpressionEvaluator.java
index c479f3faa9..360f5e80eb 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ValueExpressionDelegateValueExpressionEvaluator.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ValueExpressionDelegateValueExpressionEvaluator.java
@@ -17,6 +17,7 @@
 
 import java.util.function.Function;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.expression.ValueEvaluationContext;
 import org.springframework.data.expression.ValueExpression;
 import org.springframework.data.mapping.model.ValueExpressionEvaluator;
@@ -34,7 +35,7 @@ class ValueExpressionDelegateValueExpressionEvaluator implements ValueExpression
 
 	@SuppressWarnings("unchecked")
 	@Override
-	public <T> T evaluate(String expressionString) {
+	public <T> @Nullable T evaluate(String expressionString) {
 		ValueExpression expression = delegate.parse(expressionString);
 		return (T) expression.evaluate(expressionToContext.apply(expression));
 	}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregation.java
new file mode 100644
index 0000000000..eb8dc2e52e
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregation.java
@@ -0,0 +1,112 @@
+/*
+ * 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.mongodb.repository.query;
+
+import org.jspecify.annotations.Nullable;
+import org.springframework.data.mapping.model.ValueExpressionEvaluator;
+import org.springframework.data.mongodb.InvalidMongoDbApiUsageException;
+import org.springframework.data.mongodb.core.MongoOperations;
+import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.mongodb.repository.VectorSearch;
+import org.springframework.data.mongodb.repository.query.VectorSearchDelegate.QueryContainer;
+import org.springframework.data.mongodb.util.json.ParameterBindingContext;
+import org.springframework.data.repository.query.ResultProcessor;
+import org.springframework.data.repository.query.ValueExpressionDelegate;
+
+/**
+ * {@link AbstractMongoQuery} implementation to run a {@link VectorSearchAggregation}. The pre-filter is either derived
+ * from the method name or provided through {@link VectorSearch#filter()}.
+ *
+ * @author Mark Paluch
+ * @since 5.0
+ */
+public class VectorSearchAggregation extends AbstractMongoQuery {
+
+	private final MongoOperations mongoOperations;
+	private final MongoPersistentEntity<?> collectionEntity;
+	private final VectorSearchDelegate delegate;
+
+	/**
+	 * Creates a new {@link VectorSearchAggregation} from the given {@link MongoQueryMethod} and {@link MongoOperations}.
+	 *
+	 * @param method must not be {@literal null}.
+	 * @param mongoOperations must not be {@literal null}.
+	 * @param delegate must not be {@literal null}.
+	 */
+	public VectorSearchAggregation(MongoQueryMethod method, MongoOperations mongoOperations,
+			ValueExpressionDelegate delegate) {
+
+		super(method, mongoOperations, delegate);
+
+		if (!method.isSearchQuery() && !method.isCollectionQuery()) {
+			throw new InvalidMongoDbApiUsageException(String.format(
+					"Repository Vector Search method '%s' must return either return SearchResults<T> or List<T> but was %s",
+					method.getName(), method.getReturnType().getType().getSimpleName()));
+		}
+
+		this.mongoOperations = mongoOperations;
+		this.collectionEntity = method.getEntityInformation().getCollectionEntity();
+		this.delegate = new VectorSearchDelegate(method, mongoOperations.getConverter(), delegate);
+	}
+
+	@Override
+	protected Object doExecute(MongoQueryMethod method, ResultProcessor processor, ConvertingParameterAccessor accessor,
+			@Nullable Class<?> typeToRead) {
+
+		QueryContainer query = createVectorSearchQuery(processor, accessor, typeToRead);
+
+		MongoQueryExecution.VectorSearchExecution execution = new MongoQueryExecution.VectorSearchExecution(mongoOperations,
+				method, collectionEntity.getCollection(), query);
+
+		return execution.execute(query.query());
+	}
+
+	QueryContainer createVectorSearchQuery(ResultProcessor processor, MongoParameterAccessor accessor,
+			@Nullable Class<?> typeToRead) {
+
+		ValueExpressionEvaluator evaluator = getExpressionEvaluatorFor(accessor);
+		ParameterBindingContext bindingContext = prepareBindingContext(delegate.getQueryString(), accessor);
+
+		return delegate.createQuery(evaluator, processor, accessor, typeToRead, getParameterBindingCodec(), bindingContext);
+	}
+
+	@Override
+	protected Query createQuery(ConvertingParameterAccessor accessor) {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	protected boolean isCountQuery() {
+		return false;
+	}
+
+	@Override
+	protected boolean isExistsQuery() {
+		return false;
+	}
+
+	@Override
+	protected boolean isDeleteQuery() {
+		return false;
+	}
+
+	@Override
+	protected boolean isLimiting() {
+		return false;
+	}
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegate.java
new file mode 100644
index 0000000000..0dbff2e932
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegate.java
@@ -0,0 +1,422 @@
+/*
+ * 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.mongodb.repository.query;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.bson.Document;
+import org.jspecify.annotations.Nullable;
+import org.springframework.data.domain.Limit;
+import org.springframework.data.domain.Range;
+import org.springframework.data.domain.Score;
+import org.springframework.data.domain.ScoringFunction;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Vector;
+import org.springframework.data.expression.ValueExpression;
+import org.springframework.data.mapping.PersistentPropertyPath;
+import org.springframework.data.mapping.context.MappingContext;
+import org.springframework.data.mapping.model.ValueExpressionEvaluator;
+import org.springframework.data.mongodb.InvalidMongoDbApiUsageException;
+import org.springframework.data.mongodb.core.aggregation.Aggregation;
+import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
+import org.springframework.data.mongodb.core.aggregation.AggregationPipeline;
+import org.springframework.data.mongodb.core.aggregation.VectorSearchOperation;
+import org.springframework.data.mongodb.core.convert.MongoConverter;
+import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
+import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
+import org.springframework.data.mongodb.core.query.BasicQuery;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.mongodb.repository.VectorSearch;
+import org.springframework.data.mongodb.util.json.ParameterBindingContext;
+import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec;
+import org.springframework.data.repository.query.ResultProcessor;
+import org.springframework.data.repository.query.ValueExpressionDelegate;
+import org.springframework.data.repository.query.parser.Part;
+import org.springframework.data.repository.query.parser.PartTree;
+import org.springframework.util.NumberUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Delegate to assemble information about Vector Search queries necessary to run a MongoDB {@code $vectorSearch}.
+ *
+ * @author Mark Paluch
+ */
+class VectorSearchDelegate {
+
+	private final VectorSearchQueryFactory queryFactory;
+	private final VectorSearchOperation.SearchType searchType;
+	private final String indexName;
+	private final @Nullable Integer numCandidates;
+	private final @Nullable String numCandidatesExpression;
+	private final Limit limit;
+	private final @Nullable String limitExpression;
+	private final MongoConverter converter;
+
+	VectorSearchDelegate(MongoQueryMethod method, MongoConverter converter, ValueExpressionDelegate delegate) {
+
+		VectorSearch vectorSearch = method.findAnnotatedVectorSearch().orElseThrow();
+
+		this.searchType = vectorSearch.searchType();
+		this.indexName = method.getAnnotatedHint();
+
+		if (StringUtils.hasText(vectorSearch.numCandidates())) {
+
+			ValueExpression expression = delegate.getValueExpressionParser().parse(vectorSearch.numCandidates());
+
+			if (expression.isLiteral()) {
+				this.numCandidates = Integer.parseInt(vectorSearch.numCandidates());
+				this.numCandidatesExpression = null;
+			} else {
+				this.numCandidates = null;
+				this.numCandidatesExpression = vectorSearch.numCandidates();
+			}
+
+		} else {
+			this.numCandidates = null;
+			this.numCandidatesExpression = null;
+		}
+
+		if (StringUtils.hasText(vectorSearch.limit())) {
+
+			ValueExpression expression = delegate.getValueExpressionParser().parse(vectorSearch.limit());
+
+			if (expression.isLiteral()) {
+				this.limit = Limit.of(Integer.parseInt(vectorSearch.limit()));
+				this.limitExpression = null;
+			} else {
+				this.limit = Limit.unlimited();
+				this.limitExpression = vectorSearch.limit();
+			}
+
+		} else {
+			this.limit = Limit.unlimited();
+			this.limitExpression = null;
+		}
+
+		this.converter = converter;
+
+		if (StringUtils.hasText(vectorSearch.filter())) {
+			this.queryFactory = StringUtils.hasText(vectorSearch.path())
+					? new AnnotatedQueryFactory(vectorSearch.filter(), vectorSearch.path())
+					: new AnnotatedQueryFactory(vectorSearch.filter(), method.getEntityInformation().getCollectionEntity());
+		} else {
+			this.queryFactory = new PartTreeQueryFactory(
+					new PartTree(method.getName(), method.getResultProcessor().getReturnedType().getDomainType()),
+					converter.getMappingContext());
+		}
+	}
+
+	/**
+	 * Create Query Metadata for {@code $vectorSearch}.
+	 */
+	QueryContainer createQuery(ValueExpressionEvaluator evaluator, ResultProcessor processor,
+			MongoParameterAccessor accessor, @Nullable Class<?> typeToRead, ParameterBindingDocumentCodec codec,
+			ParameterBindingContext context) {
+
+		String scoreField = "__score__";
+		Class<?> outputType = typeToRead != null ? typeToRead : processor.getReturnedType().getReturnedType();
+		VectorSearchInput vectorSearchInput = createSearchInput(evaluator, accessor, codec, context);
+		AggregationPipeline pipeline = createVectorSearchPipeline(vectorSearchInput, scoreField, outputType, accessor,
+				evaluator);
+
+		return new QueryContainer(vectorSearchInput.path, scoreField, vectorSearchInput.query, pipeline, searchType,
+				outputType, getSimilarityFunction(accessor), indexName);
+	}
+
+	@SuppressWarnings("NullAway")
+	AggregationPipeline createVectorSearchPipeline(VectorSearchInput input, String scoreField, Class<?> outputType,
+			MongoParameterAccessor accessor, ValueExpressionEvaluator evaluator) {
+
+		Vector vector = accessor.getVector();
+		Score score = accessor.getScore();
+		Range<Score> distance = accessor.getScoreRange();
+		Limit limit = Limit.of(input.query().getLimit());
+
+		List<AggregationOperation> stages = new ArrayList<>();
+		VectorSearchOperation $vectorSearch = Aggregation.vectorSearch(indexName).path(input.path()).vector(vector)
+				.limit(limit);
+
+		Integer candidates = null;
+		if (this.numCandidatesExpression != null) {
+			candidates = ((Number) evaluator.evaluate(this.numCandidatesExpression)).intValue();
+		} else if (this.numCandidates != null) {
+			candidates = this.numCandidates;
+		} else if (input.query().isLimited() && (searchType == VectorSearchOperation.SearchType.ANN
+				|| searchType == VectorSearchOperation.SearchType.DEFAULT)) {
+
+			/*
+			MongoDB: We recommend that you specify a number at least 20 times higher than the number of documents to return (limit) to increase accuracy.
+			 */
+			candidates = input.query().getLimit() * 20;
+		}
+
+		if (candidates != null) {
+			$vectorSearch = $vectorSearch.numCandidates(candidates);
+		}
+		//
+		$vectorSearch = $vectorSearch.filter(input.query.getQueryObject());
+		$vectorSearch = $vectorSearch.searchType(this.searchType);
+		$vectorSearch = $vectorSearch.withSearchScore(scoreField);
+
+		if (score != null) {
+			$vectorSearch = $vectorSearch.withFilterBySore(c -> {
+				c.gt(score.getValue());
+			});
+		} else if (distance.getLowerBound().isBounded() || distance.getUpperBound().isBounded()) {
+			$vectorSearch = $vectorSearch.withFilterBySore(c -> {
+				Range.Bound<Score> lower = distance.getLowerBound();
+				if (lower.isBounded()) {
+					double value = lower.getValue().get().getValue();
+					if (lower.isInclusive()) {
+						c.gte(value);
+					} else {
+						c.gt(value);
+					}
+				}
+
+				Range.Bound<Score> upper = distance.getUpperBound();
+				if (upper.isBounded()) {
+
+					double value = upper.getValue().get().getValue();
+					if (upper.isInclusive()) {
+						c.lte(value);
+					} else {
+						c.lt(value);
+					}
+				}
+			});
+		}
+
+		stages.add($vectorSearch);
+
+		if (input.query().isSorted()) {
+
+			stages.add(ctx -> {
+
+				Document mappedSort = ctx.getMappedObject(input.query().getSortObject(), outputType);
+				mappedSort.append(scoreField, -1);
+				return ctx.getMappedObject(new Document("$sort", mappedSort));
+			});
+		} else {
+			stages.add(Aggregation.sort(Sort.Direction.DESC, scoreField));
+		}
+
+		return new AggregationPipeline(stages);
+	}
+
+	private VectorSearchInput createSearchInput(ValueExpressionEvaluator evaluator, MongoParameterAccessor accessor,
+			ParameterBindingDocumentCodec codec, ParameterBindingContext context) {
+
+		VectorSearchInput input = queryFactory.createQuery(accessor, codec, context);
+		Limit limit = getLimit(evaluator, accessor);
+		if(!input.query.isLimited() || (input.query.isLimited() && !limit.isUnlimited())) {
+			input.query().limit(limit);
+		}
+		return input;
+	}
+
+	private Limit getLimit(ValueExpressionEvaluator evaluator, MongoParameterAccessor accessor) {
+
+		if (this.limitExpression != null) {
+
+			Object value = evaluator.evaluate(this.limitExpression);
+			if (value != null) {
+				if (value instanceof Limit l) {
+					return l;
+				}
+				if (value instanceof Number n) {
+					return Limit.of(n.intValue());
+				}
+				if (value instanceof String s) {
+					return Limit.of(NumberUtils.parseNumber(s, Integer.class));
+				}
+				throw new IllegalArgumentException("Invalid type for Limit. Found [%s], expected Limit or Number");
+			}
+		}
+
+		if (this.limit.isLimited()) {
+			return this.limit;
+		}
+
+		return accessor.getLimit();
+	}
+
+	public String getQueryString() {
+		return queryFactory.getQueryString();
+	}
+
+	ScoringFunction getSimilarityFunction(MongoParameterAccessor accessor) {
+
+		Score score = accessor.getScore();
+
+		if (score != null) {
+			return score.getFunction();
+		}
+
+		Range<Score> scoreRange = accessor.getScoreRange();
+
+		if (scoreRange != null) {
+			if (scoreRange.getUpperBound().isBounded()) {
+				return scoreRange.getUpperBound().getValue().get().getFunction();
+			}
+
+			if (scoreRange.getLowerBound().isBounded()) {
+				return scoreRange.getLowerBound().getValue().get().getFunction();
+			}
+		}
+
+		return ScoringFunction.unspecified();
+	}
+
+	/**
+	 * Metadata for a Vector Search Aggregation.
+	 *
+	 * @param path
+	 * @param query
+	 * @param searchType
+	 * @param outputType
+	 * @param scoringFunction
+	 */
+	record QueryContainer(String path, String scoreField, Query query, AggregationPipeline pipeline,
+			VectorSearchOperation.SearchType searchType, Class<?> outputType, ScoringFunction scoringFunction, String index) {
+
+	}
+
+	/**
+	 * Strategy interface to implement a query factory for the Vector Search pre-filter query.
+	 */
+	private interface VectorSearchQueryFactory {
+
+		VectorSearchInput createQuery(MongoParameterAccessor parameterAccessor, ParameterBindingDocumentCodec codec,
+				ParameterBindingContext context);
+
+		/**
+		 * @return the underlying query string to determine {@link ParameterBindingContext}.
+		 */
+		String getQueryString();
+	}
+
+	private static class AnnotatedQueryFactory implements VectorSearchQueryFactory {
+
+		private final String query;
+		private final String path;
+
+		AnnotatedQueryFactory(String query, String path) {
+
+			this.query = query;
+			this.path = path;
+		}
+
+		AnnotatedQueryFactory(String query, MongoPersistentEntity<?> entity) {
+
+			this.query = query;
+			String path = null;
+			for (MongoPersistentProperty property : entity) {
+				if (Vector.class.isAssignableFrom(property.getType())) {
+					path = property.getFieldName();
+					break;
+				}
+			}
+
+			if (path == null) {
+				throw new InvalidMongoDbApiUsageException(
+						"Cannot find Vector Search property in entity [%s]".formatted(entity.getName()));
+			}
+
+			this.path = path;
+		}
+
+		public VectorSearchInput createQuery(MongoParameterAccessor parameterAccessor, ParameterBindingDocumentCodec codec,
+				ParameterBindingContext context) {
+
+			Document queryObject = codec.decode(this.query, context);
+			Query query = new BasicQuery(queryObject);
+
+			Sort sort = parameterAccessor.getSort();
+			if (sort.isSorted()) {
+				query = query.with(sort);
+			}
+
+			return new VectorSearchInput(path, query);
+		}
+
+		@Override
+		public String getQueryString() {
+			return this.query;
+		}
+	}
+
+	private class PartTreeQueryFactory implements VectorSearchQueryFactory {
+
+		private final String path;
+		private final PartTree tree;
+
+		@SuppressWarnings("NullableProblems")
+		PartTreeQueryFactory(PartTree tree, MappingContext<?, MongoPersistentProperty> context) {
+
+			String path = null;
+			for (PartTree.OrPart part : tree) {
+				for (Part p : part) {
+					if (p.getType() == Part.Type.SIMPLE_PROPERTY || p.getType() == Part.Type.NEAR
+							|| p.getType() == Part.Type.WITHIN || p.getType() == Part.Type.BETWEEN) {
+						PersistentPropertyPath<MongoPersistentProperty> ppp = context.getPersistentPropertyPath(p.getProperty());
+						MongoPersistentProperty property = ppp.getLeafProperty();
+
+						if (Vector.class.isAssignableFrom(property.getType())) {
+							path = p.getProperty().toDotPath();
+							break;
+						}
+					}
+				}
+			}
+
+			if (path == null) {
+				throw new InvalidMongoDbApiUsageException(
+						"No Simple Property/Near/Within/Between part found for a Vector property");
+			}
+
+			this.path = path;
+			this.tree = tree;
+		}
+
+		@SuppressWarnings("NullAway")
+		public VectorSearchInput createQuery(MongoParameterAccessor parameterAccessor, ParameterBindingDocumentCodec codec,
+				ParameterBindingContext context) {
+
+			MongoQueryCreator creator = new MongoQueryCreator(tree, parameterAccessor, converter.getMappingContext(), false,
+					true);
+
+			Query query = creator.createQuery(parameterAccessor.getSort());
+
+			if (tree.isLimiting()) {
+				query.limit(tree.getMaxResults());
+			}
+
+			return new VectorSearchInput(path, query);
+		}
+
+		@Override
+		public String getQueryString() {
+			return "";
+		}
+	}
+
+	private record VectorSearchInput(String path, Query query) {
+
+	}
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/package-info.java
index 20c77e22aa..5f0cc21049 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/package-info.java
@@ -1,6 +1,6 @@
 /**
  * Query derivation mechanism for MongoDB specific repositories.
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 package org.springframework.data.mongodb.repository.query;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/CrudMethodMetadataPostProcessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/CrudMethodMetadataPostProcessor.java
index f59a995170..037bd60672 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/CrudMethodMetadataPostProcessor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/CrudMethodMetadataPostProcessor.java
@@ -25,6 +25,7 @@
 
 import org.aopalliance.intercept.MethodInterceptor;
 import org.aopalliance.intercept.MethodInvocation;
+import org.jspecify.annotations.Nullable;
 import org.springframework.aop.TargetSource;
 import org.springframework.aop.framework.ProxyFactory;
 import org.springframework.beans.factory.BeanClassLoaderAware;
@@ -32,13 +33,13 @@
 import org.springframework.core.annotation.AnnotatedElementUtils;
 import org.springframework.data.repository.core.RepositoryInformation;
 import org.springframework.data.repository.core.support.RepositoryProxyPostProcessor;
-import org.springframework.lang.Nullable;
 import org.springframework.transaction.support.TransactionSynchronizationManager;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.ReflectionUtils;
 
 import com.mongodb.ReadPreference;
+import org.springframework.util.StringUtils;
 
 /**
  * {@link RepositoryProxyPostProcessor} that sets up interceptors to read metadata information from the invoked method.
@@ -54,7 +55,7 @@ class CrudMethodMetadataPostProcessor implements RepositoryProxyPostProcessor, B
 	private @Nullable ClassLoader classLoader = ClassUtils.getDefaultClassLoader();
 
 	@Override
-	public void setBeanClassLoader(ClassLoader classLoader) {
+	public void setBeanClassLoader(@Nullable ClassLoader classLoader) {
 		this.classLoader = classLoader;
 	}
 
@@ -121,7 +122,7 @@ static MethodInvocation currentInvocation() throws IllegalStateException {
 		}
 
 		@Override
-		public Object invoke(MethodInvocation invocation) throws Throwable {
+		public @Nullable Object invoke(MethodInvocation invocation) throws Throwable {
 
 			Method method = invocation.getMethod();
 
@@ -193,7 +194,7 @@ private static Optional<ReadPreference> findReadPreference(AnnotatedElement... a
 				org.springframework.data.mongodb.repository.ReadPreference preference = AnnotatedElementUtils
 						.findMergedAnnotation(element, org.springframework.data.mongodb.repository.ReadPreference.class);
 
-				if (preference != null) {
+				if (preference != null && StringUtils.hasText(preference.value())) {
 					return Optional.of(com.mongodb.ReadPreference.valueOf(preference.value()));
 				}
 			}
@@ -220,7 +221,7 @@ public boolean isStatic() {
 		}
 
 		@Override
-		public Object getTarget() {
+		public @Nullable Object getTarget() {
 
 			MethodInvocation invocation = CrudMethodMetadataPopulatingMethodInterceptor.currentInvocation();
 			return TransactionSynchronizationManager.getResource(invocation.getMethod());
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java
index 1d876289be..443108d2f0 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java
@@ -16,12 +16,12 @@
 package org.springframework.data.mongodb.repository.support;
 
 import org.bson.types.ObjectId;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mapping.PersistentPropertyAccessor;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
 import org.springframework.data.mongodb.core.query.Collation;
 import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
 import org.springframework.data.repository.core.support.PersistentEntityInformation;
-import org.springframework.lang.Nullable;
 
 /**
  * {@link MongoEntityInformation} implementation using a {@link MongoPersistentEntity} instance to lookup the necessary
@@ -113,7 +113,7 @@ public boolean isVersioned() {
 	}
 
 	@Override
-	public Object getVersion(T entity) {
+	public @Nullable Object getVersion(T entity) {
 
 		if (!isVersioned()) {
 			return null;
@@ -124,8 +124,7 @@ public Object getVersion(T entity) {
 		return accessor.getProperty(this.entityMetadata.getRequiredVersionProperty());
 	}
 
-	@Nullable
-	public Collation getCollation() {
+	public @Nullable Collation getCollation() {
 		return this.entityMetadata.getCollation();
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoAnnotationProcessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoAnnotationProcessor.java
index 3c029ee5aa..6deee469e1 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoAnnotationProcessor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoAnnotationProcessor.java
@@ -22,9 +22,8 @@
 import javax.annotation.processing.SupportedSourceVersion;
 import javax.lang.model.SourceVersion;
 import javax.tools.Diagnostic;
-
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.mapping.Document;
-import org.springframework.lang.Nullable;
 
 import com.querydsl.apt.AbstractQuerydslProcessor;
 import com.querydsl.apt.Configuration;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoEntityInformationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoEntityInformationSupport.java
index d0a3f7a1e4..1a39198757 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoEntityInformationSupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoEntityInformationSupport.java
@@ -15,9 +15,9 @@
  */
 package org.springframework.data.mongodb.repository.support;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
 import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
 /**
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java
index baf069c3a4..a309cea0a3 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java
@@ -15,14 +15,12 @@
  */
 package org.springframework.data.mongodb.repository.support;
 
-import static org.springframework.data.querydsl.QuerydslUtils.*;
-
-import java.io.Serializable;
 import java.lang.reflect.Method;
 import java.util.Optional;
 
+import org.jspecify.annotations.Nullable;
+
 import org.springframework.beans.factory.BeanFactory;
-import org.springframework.dao.InvalidDataAccessApiUsageException;
 import org.springframework.data.mapping.context.MappingContext;
 import org.springframework.data.mongodb.core.MongoOperations;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
@@ -33,8 +31,8 @@
 import org.springframework.data.mongodb.repository.query.PartTreeMongoQuery;
 import org.springframework.data.mongodb.repository.query.StringBasedAggregation;
 import org.springframework.data.mongodb.repository.query.StringBasedMongoQuery;
+import org.springframework.data.mongodb.repository.query.VectorSearchAggregation;
 import org.springframework.data.projection.ProjectionFactory;
-import org.springframework.data.querydsl.QuerydslPredicateExecutor;
 import org.springframework.data.repository.core.NamedQueries;
 import org.springframework.data.repository.core.RepositoryInformation;
 import org.springframework.data.repository.core.RepositoryMetadata;
@@ -42,10 +40,8 @@
 import org.springframework.data.repository.core.support.RepositoryFactorySupport;
 import org.springframework.data.repository.query.QueryLookupStrategy;
 import org.springframework.data.repository.query.QueryLookupStrategy.Key;
-import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor;
 import org.springframework.data.repository.query.RepositoryQuery;
 import org.springframework.data.repository.query.ValueExpressionDelegate;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
 /**
@@ -61,7 +57,7 @@ public class MongoRepositoryFactory extends RepositoryFactorySupport {
 	private final CrudMethodMetadataPostProcessor crudMethodMetadataPostProcessor = new CrudMethodMetadataPostProcessor();
 	private final MongoOperations operations;
 	private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
-	@Nullable private QueryMethodValueEvaluationContextAccessor accessor;
+	private MongoRepositoryFragmentsContributor fragmentsContributor = MongoRepositoryFragmentsContributor.DEFAULT;
 
 	/**
 	 * Creates a new {@link MongoRepositoryFactory} with the given {@link MongoOperations}.
@@ -78,15 +74,26 @@ public MongoRepositoryFactory(MongoOperations mongoOperations) {
 		addRepositoryProxyPostProcessor(crudMethodMetadataPostProcessor);
 	}
 
+	/**
+	 * Configures the {@link MongoRepositoryFragmentsContributor} to be used. Defaults to
+	 * {@link MongoRepositoryFragmentsContributor#DEFAULT}.
+	 *
+	 * @param fragmentsContributor
+	 * @since 5.0
+	 */
+	public void setFragmentsContributor(MongoRepositoryFragmentsContributor fragmentsContributor) {
+		this.fragmentsContributor = fragmentsContributor;
+	}
+
 	@Override
-	public void setBeanClassLoader(ClassLoader classLoader) {
+	public void setBeanClassLoader(@Nullable ClassLoader classLoader) {
 
 		super.setBeanClassLoader(classLoader);
 		crudMethodMetadataPostProcessor.setBeanClassLoader(classLoader);
 	}
 
 	@Override
-	protected ProjectionFactory getProjectionFactory(ClassLoader classLoader, BeanFactory beanFactory) {
+	protected ProjectionFactory getProjectionFactory(@Nullable ClassLoader classLoader, @Nullable BeanFactory beanFactory) {
 		return this.operations.getConverter().getProjectionFactory();
 	}
 
@@ -101,40 +108,24 @@ protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata
 	}
 
 	/**
-	 * Creates {@link RepositoryFragments} based on {@link RepositoryMetadata} to add Mongo-specific extensions. Typically
-	 * adds a {@link QuerydslMongoPredicateExecutor} if the repository interface uses Querydsl.
+	 * Creates {@link RepositoryFragments} based on {@link RepositoryMetadata} to add Mongo-specific extensions.
+	 * Typically, adds a {@link QuerydslMongoPredicateExecutor} if the repository interface uses Querydsl.
 	 * <p>
-	 * Can be overridden by subclasses to customize {@link RepositoryFragments}.
+	 * Built-in fragment contribution can be customized by configuring {@link MongoRepositoryFragmentsContributor}.
 	 *
 	 * @param metadata repository metadata.
 	 * @param operations the MongoDB operations manager.
-	 * @return
+	 * @return {@link RepositoryFragments} to be added to the repository.
 	 * @since 3.2.1
 	 */
 	protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata, MongoOperations operations) {
-
-		boolean isQueryDslRepository = QUERY_DSL_PRESENT
-				&& QuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface());
-
-		if (isQueryDslRepository) {
-
-			if (metadata.isReactiveRepository()) {
-				throw new InvalidDataAccessApiUsageException(
-						"Cannot combine Querydsl and reactive repository support in a single interface");
-			}
-
-			return RepositoryFragments
-					.just(new QuerydslMongoPredicateExecutor<>(getEntityInformation(metadata.getDomainType()), operations));
-		}
-
-		return RepositoryFragments.empty();
+		return fragmentsContributor.contribute(metadata, getEntityInformation(metadata), operations);
 	}
 
 	@Override
 	protected Object getTargetRepository(RepositoryInformation information) {
 
-		MongoEntityInformation<?, Serializable> entityInformation = getEntityInformation(information.getDomainType(),
-				information);
+		MongoEntityInformation<?, ?> entityInformation = getEntityInformation(information);
 		Object targetRepository = getTargetRepositoryViaReflection(information, entityInformation, operations);
 
 		if (targetRepository instanceof SimpleMongoRepository<?, ?> repository) {
@@ -150,16 +141,18 @@ protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable Key key
 		return Optional.of(new MongoQueryLookupStrategy(operations, mappingContext, valueExpressionDelegate));
 	}
 
+	@Deprecated
+	@Override
 	public <T, ID> MongoEntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
-		return getEntityInformation(domainClass, null);
+		MongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(domainClass);
+		return MongoEntityInformationSupport.entityInformationFor(entity, null);
 	}
 
-	private <T, ID> MongoEntityInformation<T, ID> getEntityInformation(Class<T> domainClass,
-			@Nullable RepositoryMetadata metadata) {
+	@Override
+	public MongoEntityInformation<?, ?> getEntityInformation(RepositoryMetadata metadata) {
 
-		MongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(domainClass);
-		return MongoEntityInformationSupport.<T, ID> entityInformationFor(entity,
-				metadata != null ? metadata.getIdType() : null);
+		MongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(metadata.getDomainType());
+		return MongoEntityInformationSupport.entityInformationFor(entity, metadata.getIdType());
 	}
 
 	/**
@@ -184,6 +177,8 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata,
 			if (namedQueries.hasQuery(namedQueryName)) {
 				String namedQuery = namedQueries.getQuery(namedQueryName);
 				return new StringBasedMongoQuery(namedQuery, queryMethod, operations, expressionSupport);
+			} else if (queryMethod.hasAnnotatedVectorSearch()) {
+				return new VectorSearchAggregation(queryMethod, operations, expressionSupport);
 			} else if (queryMethod.hasAnnotatedAggregation()) {
 				return new StringBasedAggregation(queryMethod, operations, expressionSupport);
 			} else if (queryMethod.hasAnnotatedQuery()) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryBean.java
index c98d38c5f5..18c7c5a13c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryBean.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryBean.java
@@ -17,24 +17,27 @@
 
 import java.io.Serializable;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mapping.context.MappingContext;
 import org.springframework.data.mongodb.core.MongoOperations;
 import org.springframework.data.mongodb.repository.MongoRepository;
 import org.springframework.data.repository.Repository;
 import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
 import org.springframework.data.repository.core.support.RepositoryFactorySupport;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
 /**
  * {@link org.springframework.beans.factory.FactoryBean} to create {@link MongoRepository} instances.
  *
  * @author Oliver Gierke
+ * @author Mark Paluch
  */
+@SuppressWarnings("NullAway")
 public class MongoRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends Serializable>
 		extends RepositoryFactoryBeanSupport<T, S, ID> {
 
 	private @Nullable MongoOperations operations;
+	private MongoRepositoryFragmentsContributor repositoryFragmentsContributor = MongoRepositoryFragmentsContributor.DEFAULT;
 	private boolean createIndexesForQueryMethods = false;
 	private boolean mappingContextConfigured = false;
 
@@ -56,6 +59,22 @@ public void setMongoOperations(MongoOperations operations) {
 		this.operations = operations;
 	}
 
+	@Override
+	public MongoRepositoryFragmentsContributor getRepositoryFragmentsContributor() {
+		return repositoryFragmentsContributor;
+	}
+
+	/**
+	 * Configures the {@link MongoRepositoryFragmentsContributor} to contribute built-in fragment functionality to the
+	 * repository.
+	 *
+	 * @param repositoryFragmentsContributor must not be {@literal null}.
+	 * @since 5.0
+	 */
+	public void setRepositoryFragmentsContributor(MongoRepositoryFragmentsContributor repositoryFragmentsContributor) {
+		this.repositoryFragmentsContributor = repositoryFragmentsContributor;
+	}
+
 	/**
 	 * Configures whether to automatically create indexes for the properties referenced in a query method.
 	 *
@@ -75,7 +94,8 @@ public void setMappingContext(MappingContext<?, ?> mappingContext) {
 	@Override
 	protected RepositoryFactorySupport createRepositoryFactory() {
 
-		RepositoryFactorySupport factory = getFactoryInstance(operations);
+		MongoRepositoryFactory factory = getFactoryInstance(operations);
+		factory.setFragmentsContributor(repositoryFragmentsContributor);
 
 		if (createIndexesForQueryMethods) {
 			factory.addQueryCreationListener(
@@ -91,7 +111,7 @@ protected RepositoryFactorySupport createRepositoryFactory() {
 	 * @param operations
 	 * @return
 	 */
-	protected RepositoryFactorySupport getFactoryInstance(MongoOperations operations) {
+	protected MongoRepositoryFactory getFactoryInstance(MongoOperations operations) {
 		return new MongoRepositoryFactory(operations);
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFragmentsContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFragmentsContributor.java
new file mode 100644
index 0000000000..6d4a409724
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFragmentsContributor.java
@@ -0,0 +1,78 @@
+/*
+ * 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.mongodb.repository.support;
+
+import org.springframework.data.mongodb.core.MongoOperations;
+import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.core.support.RepositoryComposition;
+import org.springframework.data.repository.core.support.RepositoryFragmentsContributor;
+import org.springframework.util.Assert;
+
+/**
+ * MongoDB-specific {@link RepositoryFragmentsContributor} contributing fragments based on the repository.
+ * <p>
+ * Implementations must define a no-args constructor.
+ *
+ * @author Mark Paluch
+ * @since 5.0
+ * @see QuerydslMongoPredicateExecutor
+ */
+public interface MongoRepositoryFragmentsContributor extends RepositoryFragmentsContributor {
+
+	MongoRepositoryFragmentsContributor DEFAULT = QuerydslContributor.INSTANCE;
+
+	/**
+	 * Returns a composed {@code MongoRepositoryFragmentsContributor} 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 MongoRepositoryFragmentsContributor andThen(MongoRepositoryFragmentsContributor after) {
+
+		Assert.notNull(after, "MongoRepositoryFragmentsContributor must not be null");
+
+		return new MongoRepositoryFragmentsContributor() {
+
+			@Override
+			public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata,
+					MongoEntityInformation<?, ?> entityInformation, MongoOperations operations) {
+				return MongoRepositoryFragmentsContributor.this.contribute(metadata, entityInformation, operations)
+						.append(after.contribute(metadata, entityInformation, operations));
+			}
+
+			@Override
+			public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) {
+				return MongoRepositoryFragmentsContributor.this.describe(metadata).append(after.describe(metadata));
+			}
+		};
+	}
+
+	/**
+	 * Creates {@link RepositoryComposition.RepositoryFragments} based on {@link RepositoryMetadata} to add
+	 * MongoDB-specific extensions.
+	 *
+	 * @param metadata repository metadata.
+	 * @param entityInformation must not be {@literal null}.
+	 * @param operations must not be {@literal null}.
+	 * @return {@link RepositoryComposition.RepositoryFragments} to be added to the repository.
+	 */
+	RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata,
+			MongoEntityInformation<?, ?> entityInformation, MongoOperations operations);
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslContributor.java
new file mode 100644
index 0000000000..e8460f3697
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslContributor.java
@@ -0,0 +1,70 @@
+/*
+ * 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.mongodb.repository.support;
+
+import static org.springframework.data.querydsl.QuerydslUtils.*;
+
+import org.springframework.data.mongodb.core.MongoOperations;
+import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
+import org.springframework.data.querydsl.QuerydslPredicateExecutor;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.core.support.RepositoryComposition;
+import org.springframework.data.repository.core.support.RepositoryFragment;
+import org.springframework.data.repository.core.support.RepositoryFragmentsContributor;
+
+/**
+ * MongoDB-specific {@link RepositoryFragmentsContributor} contributing Querydsl fragments if a repository implements
+ * {@link QuerydslPredicateExecutor}.
+ *
+ * @author Mark Paluch
+ * @since 5.0
+ * @see QuerydslMongoPredicateExecutor
+ */
+enum QuerydslContributor implements MongoRepositoryFragmentsContributor {
+
+	INSTANCE;
+
+	@Override
+	public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata,
+			MongoEntityInformation<?, ?> entityInformation, MongoOperations operations) {
+
+		if (isQuerydslRepository(metadata)) {
+
+			QuerydslMongoPredicateExecutor<?> executor = new QuerydslMongoPredicateExecutor<>(entityInformation, operations);
+
+			return RepositoryComposition.RepositoryFragments
+					.of(RepositoryFragment.implemented(QuerydslPredicateExecutor.class, executor));
+		}
+
+		return RepositoryComposition.RepositoryFragments.empty();
+	}
+
+	@Override
+	public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) {
+
+		if (isQuerydslRepository(metadata)) {
+			return RepositoryComposition.RepositoryFragments
+					.of(RepositoryFragment.structural(QuerydslPredicateExecutor.class, QuerydslMongoPredicateExecutor.class));
+		}
+
+		return RepositoryComposition.RepositoryFragments.empty();
+	}
+
+	private static boolean isQuerydslRepository(RepositoryMetadata metadata) {
+		return QUERY_DSL_PRESENT && QuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface());
+	}
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java
index ec845510ce..833ce69458 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java
@@ -23,6 +23,7 @@
 
 import org.bson.Document;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.dao.IncorrectResultSizeDataAccessException;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
@@ -245,12 +246,12 @@ protected <R> FluentQuerydsl<R> create(Predicate predicate, Sort sort, int limit
 		}
 
 		@Override
-		public T oneValue() {
+		public @Nullable T oneValue() {
 			return createQuery().fetchOne();
 		}
 
 		@Override
-		public T firstValue() {
+		public @Nullable T firstValue() {
 			return createQuery().fetchFirst();
 		}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java
index 3edfcdd2db..ce9820a4d9 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java
@@ -15,13 +15,11 @@
  */
 package org.springframework.data.mongodb.repository.support;
 
-import static org.springframework.data.querydsl.QuerydslUtils.*;
-
-import java.io.Serializable;
 import java.lang.reflect.Method;
 import java.util.Optional;
 
-import org.springframework.beans.BeansException;
+import org.jspecify.annotations.Nullable;
+
 import org.springframework.beans.factory.BeanFactory;
 import org.springframework.data.mapping.context.MappingContext;
 import org.springframework.data.mongodb.core.ReactiveMongoOperations;
@@ -33,21 +31,18 @@
 import org.springframework.data.mongodb.repository.query.ReactivePartTreeMongoQuery;
 import org.springframework.data.mongodb.repository.query.ReactiveStringBasedAggregation;
 import org.springframework.data.mongodb.repository.query.ReactiveStringBasedMongoQuery;
+import org.springframework.data.mongodb.repository.query.ReactiveVectorSearchAggregation;
 import org.springframework.data.projection.ProjectionFactory;
-import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor;
 import org.springframework.data.repository.core.NamedQueries;
 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.core.support.RepositoryFragment;
 import org.springframework.data.repository.query.QueryLookupStrategy;
 import org.springframework.data.repository.query.QueryLookupStrategy.Key;
 import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor;
-import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
 import org.springframework.data.repository.query.RepositoryQuery;
 import org.springframework.data.repository.query.ValueExpressionDelegate;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
 /**
@@ -63,6 +58,7 @@ public class ReactiveMongoRepositoryFactory extends ReactiveRepositoryFactorySup
 	private final CrudMethodMetadataPostProcessor crudMethodMetadataPostProcessor = new CrudMethodMetadataPostProcessor();
 	private final ReactiveMongoOperations operations;
 	private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
+	private ReactiveMongoRepositoryFragmentsContributor fragmentsContributor = ReactiveMongoRepositoryFragmentsContributor.DEFAULT;
 	@Nullable private QueryMethodValueEvaluationContextAccessor accessor;
 
 	/**
@@ -77,19 +73,30 @@ public ReactiveMongoRepositoryFactory(ReactiveMongoOperations mongoOperations) {
 		this.operations = mongoOperations;
 		this.mappingContext = mongoOperations.getConverter().getMappingContext();
 
-		setEvaluationContextProvider(ReactiveQueryMethodEvaluationContextProvider.DEFAULT);
 		addRepositoryProxyPostProcessor(crudMethodMetadataPostProcessor);
 	}
 
+	/**
+	 * Configures the {@link ReactiveMongoRepositoryFragmentsContributor} to be used. Defaults to
+	 * {@link ReactiveMongoRepositoryFragmentsContributor#DEFAULT}.
+	 *
+	 * @param fragmentsContributor
+	 * @since 5.0
+	 */
+	public void setFragmentsContributor(ReactiveMongoRepositoryFragmentsContributor fragmentsContributor) {
+		this.fragmentsContributor = fragmentsContributor;
+	}
+
 	@Override
-	public void setBeanClassLoader(ClassLoader classLoader) {
+	public void setBeanClassLoader(@Nullable ClassLoader classLoader) {
 
 		super.setBeanClassLoader(classLoader);
 		crudMethodMetadataPostProcessor.setBeanClassLoader(classLoader);
 	}
 
 	@Override
-	protected ProjectionFactory getProjectionFactory(ClassLoader classLoader, BeanFactory beanFactory) {
+	@SuppressWarnings("NullAway")
+	protected ProjectionFactory getProjectionFactory(@Nullable ClassLoader classLoader, @Nullable BeanFactory beanFactory) {
 		return this.operations.getConverter().getProjectionFactory();
 	}
 
@@ -98,30 +105,26 @@ protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
 		return SimpleReactiveMongoRepository.class;
 	}
 
+	/**
+	 * Creates {@link RepositoryFragments} based on {@link RepositoryMetadata} to add Mongo-specific extensions.
+	 * Typically, adds a {@link ReactiveQuerydslContributor} if the repository interface uses Querydsl.
+	 * <p>
+	 * Built-in fragment contribution can be customized by configuring
+	 * {@link ReactiveMongoRepositoryFragmentsContributor}.
+	 *
+	 * @param metadata repository metadata.
+	 * @return {@link RepositoryFragments} to be added to the repository.
+	 */
 	@Override
 	protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) {
-
-		RepositoryFragments fragments = RepositoryFragments.empty();
-
-		boolean isQueryDslRepository = QUERY_DSL_PRESENT
-				&& ReactiveQuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface());
-
-		if (isQueryDslRepository) {
-
-			MongoEntityInformation<?, Serializable> entityInformation = getEntityInformation(metadata.getDomainType(),
-					metadata);
-
-			fragments = fragments.append(RepositoryFragment
-					.implemented(instantiateClass(ReactiveQuerydslMongoPredicateExecutor.class, entityInformation, operations)));
-		}
-
-		return fragments;
+		return fragmentsContributor.contribute(metadata, getEntityInformation(metadata),
+				operations);
 	}
 
 	@Override
 	protected Object getTargetRepository(RepositoryInformation information) {
 
-		MongoEntityInformation<?, Serializable> entityInformation = getEntityInformation(information.getDomainType(),
+		MongoEntityInformation<?, ?> entityInformation = getEntityInformation(
 				information);
 		Object targetRepository = getTargetRepositoryViaReflection(information, entityInformation, operations);
 
@@ -132,24 +135,25 @@ protected Object getTargetRepository(RepositoryInformation information) {
 		return targetRepository;
 	}
 
-	@Override protected Optional<QueryLookupStrategy> getQueryLookupStrategy(Key key,
+	@Override
+	@SuppressWarnings("NullAway")
+	protected Optional<QueryLookupStrategy> getQueryLookupStrategy(Key key,
 			ValueExpressionDelegate valueExpressionDelegate) {
 		return Optional.of(new MongoQueryLookupStrategy(operations, mappingContext, valueExpressionDelegate));
 	}
 
+	@Deprecated
 	@Override
 	public <T, ID> MongoEntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
-		return getEntityInformation(domainClass, null);
+		MongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(domainClass);
+		return MongoEntityInformationSupport.entityInformationFor(entity, null);
 	}
 
-	@SuppressWarnings("unchecked")
-	private <T, ID> MongoEntityInformation<T, ID> getEntityInformation(Class<T> domainClass,
-			@Nullable RepositoryMetadata metadata) {
-
-		MongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(domainClass);
+	@Override
+	public MongoEntityInformation<?, ?> getEntityInformation(RepositoryMetadata metadata) {
 
-		return new MappingMongoEntityInformation<>((MongoPersistentEntity<T>) entity,
-				metadata != null ? (Class<ID>) metadata.getIdType() : null);
+		MongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(metadata.getDomainType());
+		return MongoEntityInformationSupport.entityInformationFor(entity, metadata.getIdType());
 	}
 
 	/**
@@ -159,8 +163,8 @@ private <T, ID> MongoEntityInformation<T, ID> getEntityInformation(Class<T> doma
 	 * @author Christoph Strobl
 	 */
 	private record MongoQueryLookupStrategy(ReactiveMongoOperations operations,
-		MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
-		ValueExpressionDelegate delegate) implements QueryLookupStrategy {
+			MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
+			ValueExpressionDelegate delegate) implements QueryLookupStrategy {
 
 		@Override
 		public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
@@ -174,6 +178,8 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata,
 			if (namedQueries.hasQuery(namedQueryName)) {
 				String namedQuery = namedQueries.getQuery(namedQueryName);
 				return new ReactiveStringBasedMongoQuery(namedQuery, queryMethod, operations, delegate);
+			} else if (queryMethod.hasAnnotatedVectorSearch()) {
+				return new ReactiveVectorSearchAggregation(queryMethod, operations, delegate);
 			} else if (queryMethod.hasAnnotatedAggregation()) {
 				return new ReactiveStringBasedAggregation(queryMethod, operations, delegate);
 			} else if (queryMethod.hasAnnotatedQuery()) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java
index 4f9c0d945c..40de5213aa 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java
@@ -16,18 +16,14 @@
 package org.springframework.data.mongodb.repository.support;
 
 import java.io.Serializable;
-import java.util.Optional;
 
-import org.springframework.beans.factory.ListableBeanFactory;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mapping.context.MappingContext;
 import org.springframework.data.mongodb.core.ReactiveMongoOperations;
 import org.springframework.data.mongodb.core.index.IndexOperationsAdapter;
 import org.springframework.data.repository.Repository;
 import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
 import org.springframework.data.repository.core.support.RepositoryFactorySupport;
-import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
-import org.springframework.data.repository.query.ReactiveExtensionAwareQueryMethodEvaluationContextProvider;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
 /**
@@ -44,6 +40,7 @@ public class ReactiveMongoRepositoryFactoryBean<T extends Repository<S, ID>, S,
 		extends RepositoryFactoryBeanSupport<T, S, ID> {
 
 	private @Nullable ReactiveMongoOperations operations;
+	private ReactiveMongoRepositoryFragmentsContributor repositoryFragmentsContributor = ReactiveMongoRepositoryFragmentsContributor.DEFAULT;
 	private boolean createIndexesForQueryMethods = false;
 	private boolean mappingContextConfigured = false;
 
@@ -65,6 +62,23 @@ public void setReactiveMongoOperations(@Nullable ReactiveMongoOperations operati
 		this.operations = operations;
 	}
 
+	@Override
+	public ReactiveMongoRepositoryFragmentsContributor getRepositoryFragmentsContributor() {
+		return repositoryFragmentsContributor;
+	}
+
+	/**
+	 * Configures the {@link MongoRepositoryFragmentsContributor} to contribute built-in fragment functionality to the
+	 * repository.
+	 *
+	 * @param repositoryFragmentsContributor must not be {@literal null}.
+	 * @since 5.0
+	 */
+	public void setRepositoryFragmentsContributor(
+			ReactiveMongoRepositoryFragmentsContributor repositoryFragmentsContributor) {
+		this.repositoryFragmentsContributor = repositoryFragmentsContributor;
+	}
+
 	/**
 	 * Configures whether to automatically create indexes for the properties referenced in a query method.
 	 *
@@ -82,9 +96,11 @@ public void setMappingContext(MappingContext<?, ?> mappingContext) {
 	}
 
 	@Override
+	@SuppressWarnings("NullAway")
 	protected RepositoryFactorySupport createRepositoryFactory() {
 
-		RepositoryFactorySupport factory = getFactoryInstance(operations);
+		ReactiveMongoRepositoryFactory factory = getFactoryInstance(operations);
+		factory.setFragmentsContributor(repositoryFragmentsContributor);
 
 		if (createIndexesForQueryMethods) {
 			factory.addQueryCreationListener(new IndexEnsuringQueryCreationListener(
@@ -94,19 +110,13 @@ protected RepositoryFactorySupport createRepositoryFactory() {
 		return factory;
 	}
 
-	@Override
-	protected Optional<QueryMethodEvaluationContextProvider> createDefaultQueryMethodEvaluationContextProvider(
-			ListableBeanFactory beanFactory) {
-		return Optional.of(new ReactiveExtensionAwareQueryMethodEvaluationContextProvider(beanFactory));
-	}
-
 	/**
 	 * Creates and initializes a {@link RepositoryFactorySupport} instance.
 	 *
 	 * @param operations
 	 * @return
 	 */
-	protected RepositoryFactorySupport getFactoryInstance(ReactiveMongoOperations operations) {
+	protected ReactiveMongoRepositoryFactory getFactoryInstance(ReactiveMongoOperations operations) {
 		return new ReactiveMongoRepositoryFactory(operations);
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFragmentsContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFragmentsContributor.java
new file mode 100644
index 0000000000..fdf3c3649e
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFragmentsContributor.java
@@ -0,0 +1,78 @@
+/*
+ * 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.mongodb.repository.support;
+
+import org.springframework.data.mongodb.core.ReactiveMongoOperations;
+import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.core.support.RepositoryComposition;
+import org.springframework.data.repository.core.support.RepositoryFragmentsContributor;
+import org.springframework.util.Assert;
+
+/**
+ * Reactive MongoDB-specific {@link RepositoryFragmentsContributor} contributing fragments based on the repository.
+ * <p>
+ * Implementations must define a no-args constructor.
+ *
+ * @author Mark Paluch
+ * @since 5.0
+ * @see ReactiveQuerydslMongoPredicateExecutor
+ */
+public interface ReactiveMongoRepositoryFragmentsContributor extends RepositoryFragmentsContributor {
+
+	ReactiveMongoRepositoryFragmentsContributor DEFAULT = ReactiveQuerydslContributor.INSTANCE;
+
+	/**
+	 * Returns a composed {@code ReactiveMongoRepositoryFragmentsContributor} 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 ReactiveMongoRepositoryFragmentsContributor andThen(ReactiveMongoRepositoryFragmentsContributor after) {
+
+		Assert.notNull(after, "ReactiveMongoRepositoryFragmentsContributor must not be null");
+
+		return new ReactiveMongoRepositoryFragmentsContributor() {
+
+			@Override
+			public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata,
+					MongoEntityInformation<?, ?> entityInformation, ReactiveMongoOperations operations) {
+				return ReactiveMongoRepositoryFragmentsContributor.this.contribute(metadata, entityInformation, operations)
+						.append(after.contribute(metadata, entityInformation, operations));
+			}
+
+			@Override
+			public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) {
+				return ReactiveMongoRepositoryFragmentsContributor.this.describe(metadata).append(after.describe(metadata));
+			}
+		};
+	}
+
+	/**
+	 * Creates {@link RepositoryComposition.RepositoryFragments} based on {@link RepositoryMetadata} to add
+	 * MongoDB-specific extensions.
+	 *
+	 * @param metadata repository metadata.
+	 * @param entityInformation must not be {@literal null}.
+	 * @param operations must not be {@literal null}.
+	 * @return {@link RepositoryComposition.RepositoryFragments} to be added to the repository.
+	 */
+	RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata,
+			MongoEntityInformation<?, ?> entityInformation, ReactiveMongoOperations operations);
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslContributor.java
new file mode 100644
index 0000000000..2cea75cb44
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslContributor.java
@@ -0,0 +1,73 @@
+/*
+ * 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.mongodb.repository.support;
+
+import static org.springframework.data.querydsl.QuerydslUtils.*;
+
+import org.springframework.data.mongodb.core.ReactiveMongoOperations;
+import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
+import org.springframework.data.querydsl.QuerydslPredicateExecutor;
+import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.core.support.RepositoryComposition;
+import org.springframework.data.repository.core.support.RepositoryFragment;
+import org.springframework.data.repository.core.support.RepositoryFragmentsContributor;
+
+/**
+ * Reactive MongoDB-specific {@link RepositoryFragmentsContributor} contributing Querydsl fragments if a repository
+ * implements {@link QuerydslPredicateExecutor}.
+ *
+ * @author Mark Paluch
+ * @since 5.0
+ * @see ReactiveQuerydslMongoPredicateExecutor
+ */
+enum ReactiveQuerydslContributor implements ReactiveMongoRepositoryFragmentsContributor {
+
+	INSTANCE;
+
+	@Override
+	public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata,
+			MongoEntityInformation<?, ?> entityInformation, ReactiveMongoOperations operations) {
+
+		if (isQuerydslRepository(metadata)) {
+
+			ReactiveQuerydslPredicateExecutor<?> executor = new ReactiveQuerydslMongoPredicateExecutor<>(entityInformation,
+					operations);
+
+			return RepositoryComposition.RepositoryFragments
+					.of(RepositoryFragment.implemented(ReactiveQuerydslPredicateExecutor.class, executor));
+		}
+
+		return RepositoryComposition.RepositoryFragments.empty();
+	}
+
+	@Override
+	public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) {
+
+		if (isQuerydslRepository(metadata)) {
+			return RepositoryComposition.RepositoryFragments.of(RepositoryFragment
+					.structural(ReactiveQuerydslPredicateExecutor.class, ReactiveQuerydslMongoPredicateExecutor.class));
+		}
+
+		return RepositoryComposition.RepositoryFragments.empty();
+	}
+
+	private static boolean isQuerydslRepository(RepositoryMetadata metadata) {
+		return QUERY_DSL_PRESENT
+				&& ReactiveQuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface());
+	}
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java
index cf5191fd42..a86ada0aad 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java
@@ -25,6 +25,7 @@
 import java.util.function.Consumer;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.ScrollPosition;
@@ -36,7 +37,6 @@
 import org.springframework.data.mongodb.core.mapping.FieldName;
 import org.springframework.data.mongodb.core.query.BasicQuery;
 import org.springframework.data.mongodb.core.query.Query;
-import org.springframework.lang.Nullable;
 import org.springframework.util.LinkedMultiValueMap;
 import org.springframework.util.MultiValueMap;
 import org.springframework.util.StringUtils;
@@ -304,7 +304,7 @@ static class NoMatchException extends RuntimeException {
 
 		@Override
 		public synchronized Throwable fillInStackTrace() {
-			return null;
+			return this;
 		}
 	}
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java
index 2f4c30ee7a..7e6e2fc82e 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java
@@ -27,6 +27,7 @@
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.dao.OptimisticLockingFailureException;
 import org.springframework.data.domain.Example;
 import org.springframework.data.domain.Page;
@@ -47,7 +48,6 @@
 import org.springframework.data.support.PageableExecutionUtils;
 import org.springframework.data.util.StreamUtils;
 import org.springframework.data.util.Streamable;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
 import com.mongodb.ReadPreference;
@@ -427,12 +427,12 @@ protected <R> FluentQueryByExample<S, R> create(Example<S> predicate, Sort sort,
 		}
 
 		@Override
-		public T oneValue() {
+		public @Nullable T oneValue() {
 			return createQuery().oneValue();
 		}
 
 		@Override
-		public T firstValue() {
+		public @Nullable T firstValue() {
 			return createQuery().firstValue();
 		}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java
index 1c1df2c9a1..7e4a3aa665 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java
@@ -30,6 +30,7 @@
 import java.util.function.Function;
 import java.util.function.UnaryOperator;
 
+import org.jspecify.annotations.Nullable;
 import org.reactivestreams.Publisher;
 
 import org.springframework.dao.IncorrectResultSizeDataAccessException;
@@ -49,7 +50,6 @@
 import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
 import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
 import org.springframework.data.repository.query.FluentQuery;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
 import com.mongodb.ReadPreference;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java
index 0ef6c38744..24a9342ca1 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java
@@ -22,7 +22,7 @@
 import java.util.stream.Stream;
 
 import org.bson.Document;
-
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.PageImpl;
 import org.springframework.data.domain.Pageable;
@@ -36,7 +36,6 @@
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mongodb.repository.util.SliceUtils;
 import org.springframework.data.support.PageableExecutionUtils;
-import org.springframework.lang.Nullable;
 
 import com.mysema.commons.lang.CloseableIterator;
 import com.mysema.commons.lang.EmptyCloseableIterator;
@@ -47,6 +46,7 @@
 import com.querydsl.core.types.Expression;
 import com.querydsl.core.types.OrderSpecifier;
 import com.querydsl.core.types.Predicate;
+import org.springframework.lang.Contract;
 
 /**
  * Spring Data specific simple {@link com.querydsl.core.Fetchable} {@link com.querydsl.core.SimpleQuery Query}
@@ -200,7 +200,7 @@ public Slice<T> fetchSlice(Pageable pageable) {
 	}
 
 	@Override
-	public T fetchFirst() {
+	public @Nullable T fetchFirst() {
 		try {
 			return find.matching(createQuery()).firstValue();
 		} catch (RuntimeException e) {
@@ -209,7 +209,7 @@ public T fetchFirst() {
 	}
 
 	@Override
-	public T fetchOne() {
+	public @Nullable T fetchOne() {
 		try {
 			return find.matching(createQuery()).oneValue();
 		} catch (RuntimeException e) {
@@ -279,7 +279,8 @@ protected List<Object> getIds(Class<?> targetType, Predicate condition) {
 		return mongoOperations.findDistinct(query, FieldName.ID.name(), targetType, Object.class);
 	}
 
-	private static <T> T handleException(RuntimeException e, T defaultValue) {
+	@Contract("_, !null -> !null")
+	private static <T> @Nullable T handleException(RuntimeException e, @Nullable T defaultValue) {
 
 		if (e.getClass().getName().endsWith("$NoResults")) {
 			return defaultValue;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuerySupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuerySupport.java
index a64f666f3f..64ea5f2384 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuerySupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuerySupport.java
@@ -27,6 +27,8 @@
 import com.querydsl.core.types.OrderSpecifier;
 import com.querydsl.mongodb.document.AbstractMongodbQuery;
 import com.querydsl.mongodb.document.MongodbDocumentSerializer;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Slice;
 
 /**
  * Support query type to augment Spring Data-specific {@link #toString} representations and
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java
index d9a550a0f7..756d04d0c2 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java
@@ -20,6 +20,8 @@
 import java.util.regex.Pattern;
 
 import org.bson.Document;
+import org.jspecify.annotations.NullUnmarked;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mapping.context.MappingContext;
 import org.springframework.data.mongodb.core.convert.MongoConverter;
 import org.springframework.data.mongodb.core.convert.QueryMapper;
@@ -27,7 +29,6 @@
 import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
 import org.springframework.data.util.TypeInformation;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 
@@ -48,6 +49,7 @@
  * @author Christoph Strobl
  * @author Mark Paluch
  */
+@NullUnmarked
 class SpringDataMongodbSerializer extends MongodbDocumentSerializer {
 
 	private static final String ID_KEY = FieldName.ID.name();
@@ -146,8 +148,7 @@ protected boolean isId(Path<?> arg) {
 	}
 
 	@Override
-	@Nullable
-	protected Object convert(@Nullable Path<?> path, @Nullable Constant<?> constant) {
+	protected @Nullable Object convert(@Nullable Path<?> path, @Nullable Constant<?> constant) {
 
 		if (constant == null) {
 			return null;
@@ -191,8 +192,7 @@ protected Object convert(@Nullable Path<?> path, @Nullable Constant<?> constant)
 		return asReference(constant.getConstant(), path);
 	}
 
-	@Nullable
-	private MongoPersistentProperty getPropertyFor(Path<?> path) {
+	private @Nullable MongoPersistentProperty getPropertyFor(Path<?> path) {
 
 		Path<?> parent = path.getMetadata().getParent();
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/package-info.java
index 1d0b8beeba..42cd5a0b18 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/package-info.java
@@ -1,6 +1,6 @@
 /**
  * Support infrastructure for query derivation of MongoDB specific repositories.
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 package org.springframework.data.mongodb.repository.support;
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java
index cbbd4a37a9..dc51da84ed 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java
@@ -29,22 +29,52 @@
 import java.util.function.Function;
 import java.util.stream.StreamSupport;
 
-import org.bson.*;
+import org.bson.AbstractBsonWriter;
+import org.bson.BSONObject;
+import org.bson.BsonArray;
+import org.bson.BsonBinary;
+import org.bson.BsonBinarySubType;
+import org.bson.BsonBoolean;
+import org.bson.BsonContextType;
+import org.bson.BsonDateTime;
+import org.bson.BsonDbPointer;
+import org.bson.BsonDecimal128;
+import org.bson.BsonDouble;
+import org.bson.BsonInt32;
+import org.bson.BsonInt64;
+import org.bson.BsonJavaScript;
+import org.bson.BsonNull;
+import org.bson.BsonObjectId;
+import org.bson.BsonReader;
+import org.bson.BsonRegularExpression;
+import org.bson.BsonString;
+import org.bson.BsonSymbol;
+import org.bson.BsonTimestamp;
+import org.bson.BsonUndefined;
+import org.bson.BsonValue;
+import org.bson.BsonWriter;
+import org.bson.BsonWriterSettings;
+import org.bson.Document;
 import org.bson.codecs.Codec;
+import org.bson.codecs.DecoderContext;
 import org.bson.codecs.DocumentCodec;
 import org.bson.codecs.EncoderContext;
 import org.bson.codecs.configuration.CodecConfigurationException;
+import org.bson.codecs.configuration.CodecRegistries;
 import org.bson.codecs.configuration.CodecRegistry;
 import org.bson.conversions.Bson;
 import org.bson.json.JsonParseException;
 import org.bson.types.Binary;
 import org.bson.types.Decimal128;
 import org.bson.types.ObjectId;
+import org.jspecify.annotations.NullUnmarked;
+import org.jspecify.annotations.Nullable;
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.data.mongodb.CodecRegistryProvider;
 import org.springframework.data.mongodb.core.mapping.FieldName;
 import org.springframework.data.mongodb.core.mapping.FieldName.Type;
-import org.springframework.lang.Nullable;
+import org.springframework.data.mongodb.core.query.CriteriaDefinition.Placeholder;
+import org.springframework.lang.Contract;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.CollectionUtils;
@@ -72,9 +102,12 @@ public class BsonUtils {
 	 */
 	public static final Document EMPTY_DOCUMENT = new EmptyDocument();
 
+	private static final CodecRegistry JSON_CODEC_REGISTRY = CodecRegistries.fromRegistries(
+			MongoClientSettings.getDefaultCodecRegistry(), CodecRegistries.fromCodecs(new PlaceholderCodec()));
+
 	@SuppressWarnings("unchecked")
-	@Nullable
-	public static <T> T get(Bson bson, String key) {
+	@Contract("null, _ -> null")
+	public static <T> @Nullable T get(@Nullable Bson bson, String key) {
 		return (T) asMap(bson).get(key);
 	}
 
@@ -85,7 +118,7 @@ public static <T> T get(Bson bson, String key) {
 	 * @param bson
 	 * @return
 	 */
-	public static Map<String, Object> asMap(Bson bson) {
+	public static Map<String, Object> asMap(@Nullable Bson bson) {
 		return asMap(bson, MongoClientSettings.getDefaultCodecRegistry());
 	}
 
@@ -126,7 +159,7 @@ public static Map<String, Object> asMap(@Nullable Bson bson, CodecRegistry codec
 	 * @return
 	 * @since 3.2.5
 	 */
-	public static Document asDocument(Bson bson) {
+	public static Document asDocument(@Nullable Bson bson) {
 		return asDocument(bson, MongoClientSettings.getDefaultCodecRegistry());
 	}
 
@@ -140,7 +173,7 @@ public static Document asDocument(Bson bson) {
 	 * @return never {@literal null}.
 	 * @since 4.0
 	 */
-	public static Document asDocument(Bson bson, CodecRegistry codecRegistry) {
+	public static Document asDocument(@Nullable Bson bson, CodecRegistry codecRegistry) {
 
 		Map<String, Object> map = asMap(bson, codecRegistry);
 
@@ -304,7 +337,7 @@ public static Object toJavaType(BsonValue value) {
 			case BINARY -> {
 
 				BsonBinary binary = value.asBinary();
-				if(binary.getType() != BsonBinarySubType.VECTOR.getValue()) {
+				if (binary.getType() != BsonBinarySubType.VECTOR.getValue()) {
 					yield binary.getData();
 				}
 				yield value.asBinary().asVector();
@@ -326,14 +359,14 @@ public static Object toJavaType(BsonValue value) {
 	 * @throws IllegalArgumentException if {@literal source} does not correspond to a {@link BsonValue} type.
 	 * @since 3.0
 	 */
-	public static BsonValue simpleToBsonValue(Object source) {
+	public static BsonValue simpleToBsonValue(@Nullable Object source) {
 		return simpleToBsonValue(source, MongoClientSettings.getDefaultCodecRegistry());
 	}
 
 	/**
 	 * Convert a given simple value (eg. {@link String}, {@link Long}) to its corresponding {@link BsonValue}.
 	 *
-	 * @param source must not be {@literal null}.
+	 * @param source can be {@literal null}.
 	 * @param codecRegistry The {@link CodecRegistry} used as a fallback to convert types using native {@link Codec}. Must
 	 *          not be {@literal null}.
 	 * @return the corresponding {@link BsonValue} representation.
@@ -341,7 +374,12 @@ public static BsonValue simpleToBsonValue(Object source) {
 	 * @since 4.2
 	 */
 	@SuppressWarnings("unchecked")
-	public static BsonValue simpleToBsonValue(Object source, CodecRegistry codecRegistry) {
+	@Contract("null, _ -> !null")
+	public static BsonValue simpleToBsonValue(@Nullable Object source, CodecRegistry codecRegistry) {
+
+		if(source == null) {
+			return BsonNull.VALUE;
+		}
 
 		if (source instanceof BsonValue bsonValue) {
 			return bsonValue;
@@ -398,7 +436,9 @@ public static BsonValue simpleToBsonValue(Object source, CodecRegistry codecRegi
 			BsonCapturingWriter writer = new BsonCapturingWriter(value.getClass());
 			codec.encode(writer, value,
 					ObjectUtils.isArray(value) || value instanceof Collection<?> ? EncoderContext.builder().build() : null);
-			return writer.getCapturedValue();
+			Object captured = writer.getCapturedValue();
+			return captured instanceof BsonValue bv ? bv : BsonNull.VALUE;
+
 		} catch (CodecConfigurationException e) {
 			throw new IllegalArgumentException(
 					String.format("Unable to convert %s to BsonValue.", source != null ? source.getClass().getName() : "null"));
@@ -450,8 +490,7 @@ public static Document toDocumentOrElse(String source, Function<String, Document
 	 * @return
 	 * @since 2.2.1
 	 */
-	@Nullable
-	public static String toJson(@Nullable Document source) {
+	public static @Nullable String toJson(@Nullable Document source) {
 
 		if (source == null) {
 			return null;
@@ -471,6 +510,7 @@ public static String toJson(@Nullable Document source) {
 	 * @return {@literal true} if the given value looks like a json document.
 	 * @since 3.0
 	 */
+	@Contract("null -> false")
 	public static boolean isJsonDocument(@Nullable String value) {
 
 		if (!StringUtils.hasText(value)) {
@@ -488,6 +528,7 @@ public static boolean isJsonDocument(@Nullable String value) {
 	 * @return {@literal true} if the given value looks like a json array.
 	 * @since 3.0
 	 */
+	@Contract("null -> false")
 	public static boolean isJsonArray(@Nullable String value) {
 		return StringUtils.hasText(value) && (value.startsWith("[") && value.endsWith("]"));
 	}
@@ -525,8 +566,7 @@ public static Document parse(String json, @Nullable CodecRegistryProvider codecR
 	 * @return can be {@literal null}.
 	 * @since 3.0.8
 	 */
-	@Nullable
-	public static Object resolveValue(Bson bson, String key) {
+	public static @Nullable Object resolveValue(Bson bson, String key) {
 		return resolveValue(asMap(bson), key);
 	}
 
@@ -541,7 +581,7 @@ public static Object resolveValue(Bson bson, String key) {
 	 * @return can be {@literal null}.
 	 * @since 4.2
 	 */
-	public static Object resolveValue(Bson bson, FieldName fieldName) {
+	public static @Nullable Object resolveValue(Bson bson, FieldName fieldName) {
 		return resolveValue(asMap(bson), fieldName);
 	}
 
@@ -556,8 +596,7 @@ public static Object resolveValue(Bson bson, FieldName fieldName) {
 	 * @return can be {@literal null}.
 	 * @since 4.2
 	 */
-	@Nullable
-	public static Object resolveValue(Map<String, Object> source, FieldName fieldName) {
+	public static @Nullable Object resolveValue(Map<String, Object> source, FieldName fieldName) {
 
 		if (fieldName.isKey()) {
 			return source.get(fieldName.name());
@@ -590,8 +629,7 @@ public static Object resolveValue(Map<String, Object> source, FieldName fieldNam
 	 * @return can be {@literal null}.
 	 * @since 4.1
 	 */
-	@Nullable
-	public static Object resolveValue(Map<String, Object> source, String key) {
+	public static @Nullable Object resolveValue(Map<String, Object> source, String key) {
 
 		if (source.containsKey(key)) {
 			return source.get(key);
@@ -643,9 +681,9 @@ public static boolean hasValue(Bson bson, String key) {
 	 * @param source can be {@literal null}.
 	 * @return can be {@literal null}.
 	 */
-	@Nullable
 	@SuppressWarnings("unchecked")
-	private static Map<String, Object> getAsMap(Object source) {
+	@Contract("null -> null")
+	private static @Nullable Map<String, Object> getAsMap(@Nullable Object source) {
 
 		if (source instanceof Document document) {
 			return document;
@@ -745,8 +783,43 @@ public static Document mapEntries(Document source, Function<Entry<String, Object
 		return new Document(target);
 	}
 
-	@Nullable
-	private static String toJson(@Nullable Object value) {
+	/**
+	 * Obtain a preconfigured {@link JsonWriter} allowing to render the given {@link Document} using a
+	 * {@link CodecRegistry} containing a {@link PlaceholderCodec}.
+	 *
+	 * @param document the source document. Must not be {@literal null}.
+	 * @return new instance of {@link JsonWriter}.
+	 * @since 5.0
+	 */
+	public static JsonWriter writeJson(Document document) {
+		return sink -> JSON_CODEC_REGISTRY.get(Document.class).encode(new SpringJsonWriter(sink), document,
+				EncoderContext.builder().build());
+	}
+
+	/**
+	 * Interface to pipe json rendering to a given sink.
+	 *
+	 * @since 5.0
+	 */
+	public interface JsonWriter {
+
+		/**
+		 * Write the json output to the given sink.
+		 *
+		 * @param sink the output target
+		 */
+		void to(StringBuffer sink);
+
+		default String toJsonString() {
+
+			StringBuffer buffer = new StringBuffer();
+			to(buffer);
+			return buffer.toString();
+		}
+	}
+
+	@Contract("null -> null")
+	private static @Nullable String toJson(@Nullable Object value) {
 
 		if (value == null) {
 			return null;
@@ -957,4 +1030,34 @@ public void flush() {
 			values.clear();
 		}
 	}
+
+	/**
+	 * Internal {@link Codec} implementation to write
+	 * {@link org.springframework.data.mongodb.core.query.CriteriaDefinition.Placeholder placeholders}.
+	 *
+	 * @since 5.0
+	 * @author Christoph Strobl
+	 */
+	@NullUnmarked
+	static class PlaceholderCodec implements Codec<Placeholder> {
+
+		@Override
+		public Placeholder decode(BsonReader reader, DecoderContext decoderContext) {
+			return null;
+		}
+
+		@Override
+		public void encode(BsonWriter writer, Placeholder value, EncoderContext encoderContext) {
+			if (writer instanceof SpringJsonWriter sjw) {
+				sjw.writePlaceholder(value.toString());
+			} else {
+				writer.writeString(value.toString());
+			}
+		}
+
+		@Override
+		public Class<Placeholder> getEncoderClass() {
+			return Placeholder.class;
+		}
+	}
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DotPath.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DotPath.java
index 191c7d24d3..549c7ff720 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DotPath.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DotPath.java
@@ -15,7 +15,7 @@
  */
 package org.springframework.data.mongodb.util;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.StringUtils;
 
 /**
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DurationUtil.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DurationUtil.java
index 67255b878a..78eb59a461 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DurationUtil.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DurationUtil.java
@@ -18,6 +18,7 @@
 import java.time.Duration;
 import java.util.function.Supplier;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.core.env.Environment;
 import org.springframework.data.expression.ValueEvaluationContext;
 import org.springframework.data.expression.ValueExpression;
@@ -25,7 +26,6 @@
 import org.springframework.expression.EvaluationContext;
 import org.springframework.expression.spel.standard.SpelExpressionParser;
 import org.springframework.format.datetime.standard.DurationFormatterUtils;
-import org.springframework.lang.Nullable;
 
 /**
  * Helper to evaluate Duration from expressions.
@@ -70,13 +70,11 @@ public static Duration evaluate(String value, ValueEvaluationContext evaluationC
 	public static Duration evaluate(String value, Supplier<EvaluationContext> evaluationContext) {
 
 		return evaluate(value, new ValueEvaluationContext() {
-			@Nullable
 			@Override
 			public Environment getEnvironment() {
-				return null;
+				throw new IllegalStateException();
 			}
 
-			@Nullable
 			@Override
 			public EvaluationContext getEvaluationContext() {
 				return evaluationContext.get();
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/EmptyDocument.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/EmptyDocument.java
index ffc97402fe..23ea9409cc 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/EmptyDocument.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/EmptyDocument.java
@@ -66,9 +66,8 @@ public boolean replace(String key, Object oldValue, Object newValue) {
 		throw new UnsupportedOperationException();
 	}
 
-	@Nullable
 	@Override
-	public Object replace(String key, Object value) {
+	public @Nullable Object replace(String key, Object value) {
 		throw new UnsupportedOperationException();
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java
index 8fc4b108ff..fbbba59e8f 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java
@@ -15,12 +15,9 @@
  */
 package org.springframework.data.mongodb.util;
 
-import java.lang.reflect.Field;
-
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.util.Version;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ClassUtils;
-import org.springframework.util.ReflectionUtils;
 
 import com.mongodb.internal.build.MongoDriverVersion;
 
@@ -94,14 +91,12 @@ private static Version getMongoDbDriverVersion(ClassLoader classLoader) {
 		return version == null ? guessDriverVersionFromClassPath(classLoader) : version;
 	}
 
-	@Nullable
-	private static Version getVersionFromPackage(ClassLoader classLoader) {
+	private static @Nullable Version getVersionFromPackage(ClassLoader classLoader) {
 
 		if (ClassUtils.isPresent("com.mongodb.internal.build.MongoDriverVersion", classLoader)) {
 			try {
-				Field field = ReflectionUtils.findField(MongoDriverVersion.class, "VERSION");
-				return field != null ? Version.parse("" + field.get(null)) : null;
-			} catch (ReflectiveOperationException | IllegalArgumentException exception) {
+				return Version.parse(MongoDriverVersion.VERSION);
+			} catch (IllegalArgumentException exception) {
 				// well not much we can do, right?
 			}
 		}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java
deleted file mode 100644
index f85be98c1f..0000000000
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java
+++ /dev/null
@@ -1,380 +0,0 @@
-/*
- * Copyright 2024-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.mongodb.util;
-
-import java.lang.reflect.Method;
-import java.net.InetSocketAddress;
-
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-import org.reactivestreams.Publisher;
-import org.springframework.lang.Nullable;
-import org.springframework.util.Assert;
-import org.springframework.util.ClassUtils;
-import org.springframework.util.ReflectionUtils;
-
-import com.mongodb.MongoClientSettings;
-import com.mongodb.MongoClientSettings.Builder;
-import com.mongodb.ServerAddress;
-import com.mongodb.client.ClientSession;
-import com.mongodb.client.MapReduceIterable;
-import com.mongodb.client.MongoDatabase;
-import com.mongodb.client.MongoIterable;
-import com.mongodb.client.model.IndexOptions;
-import com.mongodb.reactivestreams.client.MapReducePublisher;
-
-/**
- * Compatibility adapter to bridge functionality across different MongoDB driver versions.
- * <p>
- * This class is for internal use within the framework and should not be used by applications.
- *
- * @author Christoph Strobl
- * @since 4.3
- */
-public class MongoCompatibilityAdapter {
-
-	private static final String NO_LONGER_SUPPORTED = "%s is no longer supported on Mongo Client 5 or newer";
-
-	private static final @Nullable Method getStreamFactoryFactory = ReflectionUtils.findMethod(MongoClientSettings.class,
-			"getStreamFactoryFactory");
-
-	private static final @Nullable Method setBucketSize = ReflectionUtils.findMethod(IndexOptions.class, "bucketSize",
-			Double.class);
-
-	/**
-	 * Return a compatibility adapter for {@link MongoClientSettings.Builder}.
-	 *
-	 * @param builder
-	 * @return
-	 */
-	public static ClientSettingsBuilderAdapter clientSettingsBuilderAdapter(MongoClientSettings.Builder builder) {
-		return new MongoStreamFactoryFactorySettingsConfigurer(builder)::setStreamFactory;
-	}
-
-	/**
-	 * Return a compatibility adapter for {@link MongoClientSettings}.
-	 *
-	 * @param clientSettings
-	 * @return
-	 */
-	public static ClientSettingsAdapter clientSettingsAdapter(MongoClientSettings clientSettings) {
-		return new ClientSettingsAdapter() {
-			@Override
-			public <T> T getStreamFactoryFactory() {
-
-				if (MongoClientVersion.isVersion5orNewer() || getStreamFactoryFactory == null) {
-					return null;
-				}
-
-				return (T) ReflectionUtils.invokeMethod(getStreamFactoryFactory, clientSettings);
-			}
-		};
-	}
-
-	/**
-	 * Return a compatibility adapter for {@link IndexOptions}.
-	 *
-	 * @param options
-	 * @return
-	 */
-	public static IndexOptionsAdapter indexOptionsAdapter(IndexOptions options) {
-		return bucketSize -> {
-
-			if (MongoClientVersion.isVersion5orNewer() || setBucketSize == null) {
-				throw new UnsupportedOperationException(NO_LONGER_SUPPORTED.formatted("IndexOptions.bucketSize"));
-			}
-
-			ReflectionUtils.invokeMethod(setBucketSize, options, bucketSize);
-		};
-	}
-
-	/**
-	 * Return a compatibility adapter for {@code MapReduceIterable}.
-	 *
-	 * @param iterable
-	 * @return
-	 */
-	@SuppressWarnings("deprecation")
-	public static MapReduceIterableAdapter mapReduceIterableAdapter(Object iterable) {
-		return sharded -> {
-
-			if (MongoClientVersion.isVersion5orNewer()) {
-				throw new UnsupportedOperationException(NO_LONGER_SUPPORTED.formatted("sharded"));
-			}
-
-			// Use MapReduceIterable to avoid package-protected access violations to
-			// com.mongodb.client.internal.MapReduceIterableImpl
-			Method shardedMethod = ReflectionUtils.findMethod(MapReduceIterable.class, "sharded", boolean.class);
-			ReflectionUtils.invokeMethod(shardedMethod, iterable, sharded);
-		};
-	}
-
-	/**
-	 * Return a compatibility adapter for {@code MapReducePublisher}.
-	 *
-	 * @param publisher
-	 * @return
-	 */
-	@SuppressWarnings("deprecation")
-	public static MapReducePublisherAdapter mapReducePublisherAdapter(Object publisher) {
-		return sharded -> {
-
-			if (MongoClientVersion.isVersion5orNewer()) {
-				throw new UnsupportedOperationException(NO_LONGER_SUPPORTED.formatted("sharded"));
-			}
-
-			// Use MapReducePublisher to avoid package-protected access violations to MapReducePublisherImpl
-			Method shardedMethod = ReflectionUtils.findMethod(MapReducePublisher.class, "sharded", boolean.class);
-			ReflectionUtils.invokeMethod(shardedMethod, publisher, sharded);
-		};
-	}
-
-	/**
-	 * Return a compatibility adapter for {@link ServerAddress}.
-	 *
-	 * @param serverAddress
-	 * @return
-	 */
-	public static ServerAddressAdapter serverAddressAdapter(ServerAddress serverAddress) {
-		return () -> {
-
-			if (MongoClientVersion.isVersion5orNewer()) {
-				return null;
-			}
-
-			Method serverAddressMethod = ReflectionUtils.findMethod(ServerAddress.class, "getSocketAddress");
-			Object value = ReflectionUtils.invokeMethod(serverAddressMethod, serverAddress);
-			return value != null ? InetSocketAddress.class.cast(value) : null;
-		};
-	}
-
-	public static MongoDatabaseAdapterBuilder mongoDatabaseAdapter() {
-		return MongoDatabaseAdapter::new;
-	}
-
-	public static ReactiveMongoDatabaseAdapterBuilder reactiveMongoDatabaseAdapter() {
-		return ReactiveMongoDatabaseAdapter::new;
-	}
-
-	public interface IndexOptionsAdapter {
-		void setBucketSize(double bucketSize);
-	}
-
-	public interface ClientSettingsAdapter {
-		@Nullable
-		<T> T getStreamFactoryFactory();
-	}
-
-	public interface ClientSettingsBuilderAdapter {
-		<T> void setStreamFactoryFactory(T streamFactory);
-	}
-
-	public interface MapReduceIterableAdapter {
-		void sharded(boolean sharded);
-	}
-
-	public interface MapReducePublisherAdapter {
-		void sharded(boolean sharded);
-	}
-
-	public interface ServerAddressAdapter {
-		@Nullable
-		InetSocketAddress getSocketAddress();
-	}
-
-	public interface MongoDatabaseAdapterBuilder {
-		MongoDatabaseAdapter forDb(com.mongodb.client.MongoDatabase db);
-	}
-
-	@SuppressWarnings({ "unchecked", "DataFlowIssue" })
-	public static class MongoDatabaseAdapter {
-
-		@Nullable //
-		private static final Method LIST_COLLECTION_NAMES_METHOD;
-
-		@Nullable //
-		private static final Method LIST_COLLECTION_NAMES_METHOD_SESSION;
-
-		private static final Class<?> collectionNamesReturnType;
-
-		private final MongoDatabase db;
-
-		static {
-
-			if (MongoClientVersion.isSyncClientPresent()) {
-
-				LIST_COLLECTION_NAMES_METHOD = ReflectionUtils.findMethod(MongoDatabase.class, "listCollectionNames");
-				LIST_COLLECTION_NAMES_METHOD_SESSION = ReflectionUtils.findMethod(MongoDatabase.class, "listCollectionNames",
-						ClientSession.class);
-
-				if (MongoClientVersion.isVersion5orNewer()) {
-					try {
-						collectionNamesReturnType = ClassUtils.forName("com.mongodb.client.ListCollectionNamesIterable",
-								MongoDatabaseAdapter.class.getClassLoader());
-					} catch (ClassNotFoundException e) {
-						throw new IllegalStateException("Unable to load com.mongodb.client.ListCollectionNamesIterable", e);
-					}
-				} else {
-					try {
-						collectionNamesReturnType = ClassUtils.forName("com.mongodb.client.MongoIterable",
-								MongoDatabaseAdapter.class.getClassLoader());
-					} catch (ClassNotFoundException e) {
-						throw new IllegalStateException("Unable to load com.mongodb.client.ListCollectionNamesIterable", e);
-					}
-				}
-			} else {
-				LIST_COLLECTION_NAMES_METHOD = null;
-				LIST_COLLECTION_NAMES_METHOD_SESSION = null;
-				collectionNamesReturnType = Object.class;
-			}
-		}
-
-		public MongoDatabaseAdapter(MongoDatabase db) {
-			this.db = db;
-		}
-
-		public Class<? extends MongoIterable<String>> collectionNameIterableType() {
-			return (Class<? extends MongoIterable<String>>) collectionNamesReturnType;
-		}
-
-		public MongoIterable<String> listCollectionNames() {
-
-			Assert.state(LIST_COLLECTION_NAMES_METHOD != null, "No method listCollectionNames present for %s".formatted(db));
-			return (MongoIterable<String>) ReflectionUtils.invokeMethod(LIST_COLLECTION_NAMES_METHOD, db);
-		}
-
-		public MongoIterable<String> listCollectionNames(ClientSession clientSession) {
-			Assert.state(LIST_COLLECTION_NAMES_METHOD != null,
-					"No method listCollectionNames(ClientSession) present for %s".formatted(db));
-			return (MongoIterable<String>) ReflectionUtils.invokeMethod(LIST_COLLECTION_NAMES_METHOD_SESSION, db,
-					clientSession);
-		}
-	}
-
-	public interface ReactiveMongoDatabaseAdapterBuilder {
-		ReactiveMongoDatabaseAdapter forDb(com.mongodb.reactivestreams.client.MongoDatabase db);
-	}
-
-	@SuppressWarnings({ "unchecked", "DataFlowIssue" })
-	public static class ReactiveMongoDatabaseAdapter {
-
-		@Nullable //
-		private static final Method LIST_COLLECTION_NAMES_METHOD;
-
-		@Nullable //
-		private static final Method LIST_COLLECTION_NAMES_METHOD_SESSION;
-
-		private static final Class<?> collectionNamesReturnType;
-
-		private final com.mongodb.reactivestreams.client.MongoDatabase db;
-
-		static {
-
-			if (MongoClientVersion.isReactiveClientPresent()) {
-
-				LIST_COLLECTION_NAMES_METHOD = ReflectionUtils
-						.findMethod(com.mongodb.reactivestreams.client.MongoDatabase.class, "listCollectionNames");
-				LIST_COLLECTION_NAMES_METHOD_SESSION = ReflectionUtils.findMethod(
-						com.mongodb.reactivestreams.client.MongoDatabase.class, "listCollectionNames",
-						com.mongodb.reactivestreams.client.ClientSession.class);
-
-				if (MongoClientVersion.isVersion5orNewer()) {
-					try {
-						collectionNamesReturnType = ClassUtils.forName(
-								"com.mongodb.reactivestreams.client.ListCollectionNamesPublisher",
-								ReactiveMongoDatabaseAdapter.class.getClassLoader());
-					} catch (ClassNotFoundException e) {
-						throw new IllegalStateException("com.mongodb.reactivestreams.client.ListCollectionNamesPublisher", e);
-					}
-				} else {
-					try {
-						collectionNamesReturnType = ClassUtils.forName("org.reactivestreams.Publisher",
-								ReactiveMongoDatabaseAdapter.class.getClassLoader());
-					} catch (ClassNotFoundException e) {
-						throw new IllegalStateException("org.reactivestreams.Publisher", e);
-					}
-				}
-			} else {
-				LIST_COLLECTION_NAMES_METHOD = null;
-				LIST_COLLECTION_NAMES_METHOD_SESSION = null;
-				collectionNamesReturnType = Object.class;
-			}
-		}
-
-		ReactiveMongoDatabaseAdapter(com.mongodb.reactivestreams.client.MongoDatabase db) {
-			this.db = db;
-		}
-
-		public Class<? extends Publisher<String>> collectionNamePublisherType() {
-			return (Class<? extends Publisher<String>>) collectionNamesReturnType;
-
-		}
-
-		public Publisher<String> listCollectionNames() {
-			Assert.state(LIST_COLLECTION_NAMES_METHOD != null, "No method listCollectionNames present for %s".formatted(db));
-			return (Publisher<String>) ReflectionUtils.invokeMethod(LIST_COLLECTION_NAMES_METHOD, db);
-		}
-
-		public Publisher<String> listCollectionNames(com.mongodb.reactivestreams.client.ClientSession clientSession) {
-			Assert.state(LIST_COLLECTION_NAMES_METHOD != null,
-					"No method listCollectionNames(ClientSession) present for %s".formatted(db));
-			return (Publisher<String>) ReflectionUtils.invokeMethod(LIST_COLLECTION_NAMES_METHOD_SESSION, db, clientSession);
-		}
-	}
-
-	static class MongoStreamFactoryFactorySettingsConfigurer {
-
-		private static final Log logger = LogFactory.getLog(MongoStreamFactoryFactorySettingsConfigurer.class);
-
-		private static final String STREAM_FACTORY_NAME = "com.mongodb.connection.StreamFactoryFactory";
-		private static final boolean STREAM_FACTORY_PRESENT = ClassUtils.isPresent(STREAM_FACTORY_NAME,
-				MongoCompatibilityAdapter.class.getClassLoader());
-		private final MongoClientSettings.Builder settingsBuilder;
-
-		static boolean isStreamFactoryPresent() {
-			return STREAM_FACTORY_PRESENT;
-		}
-
-		public MongoStreamFactoryFactorySettingsConfigurer(Builder settingsBuilder) {
-			this.settingsBuilder = settingsBuilder;
-		}
-
-		void setStreamFactory(Object streamFactory) {
-
-			if (MongoClientVersion.isVersion5orNewer() && isStreamFactoryPresent()) {
-				logger.warn("StreamFactoryFactory is no longer available. Use TransportSettings instead.");
-				return;
-			}
-
-			try {
-				Class<?> streamFactoryType = ClassUtils.forName(STREAM_FACTORY_NAME, streamFactory.getClass().getClassLoader());
-
-				if (!ClassUtils.isAssignable(streamFactoryType, streamFactory.getClass())) {
-					throw new IllegalArgumentException("Expected %s but found %s".formatted(streamFactoryType, streamFactory));
-				}
-
-				Method setter = ReflectionUtils.findMethod(settingsBuilder.getClass(), "streamFactoryFactory",
-						streamFactoryType);
-				if (setter != null) {
-					ReflectionUtils.invokeMethod(setter, settingsBuilder, streamFactoryType.cast(streamFactory));
-				}
-			} catch (ReflectiveOperationException e) {
-				throw new IllegalArgumentException("Cannot set StreamFactoryFactory for %s".formatted(settingsBuilder), e);
-			}
-		}
-	}
-
-}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoDbErrorCodes.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoDbErrorCodes.java
index 326a5c1e88..7fcc1383d5 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoDbErrorCodes.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoDbErrorCodes.java
@@ -15,12 +15,14 @@
  */
 package org.springframework.data.mongodb.util;
 
+import java.util.Collections;
 import java.util.HashMap;
-
-import org.springframework.lang.Nullable;
+import java.util.Map;
 
 import com.mongodb.MongoException;
 
+import org.jspecify.annotations.Nullable;
+
 /**
  * {@link MongoDbErrorCodes} holds MongoDB specific error codes outlined in {@literal mongo/base/error_codes.yml}.
  *
@@ -128,7 +130,9 @@ public final class MongoDbErrorCodes {
 		clientSessionCodes.put(263, "OperationNotSupportedInTransaction");
 		clientSessionCodes.put(264, "TooManyLogicalSessions");
 
-		errorCodes = new HashMap<>(
+		transactionCodes = new HashMap<>(0);
+
+ 		errorCodes = new HashMap<>(
 				dataAccessResourceFailureCodes.size() + dataIntegrityViolationCodes.size() + duplicateKeyCodes.size()
 						+ invalidDataAccessApiUsageException.size() + permissionDeniedCodes.size() + clientSessionCodes.size(),
 				1f);
@@ -140,8 +144,7 @@ public final class MongoDbErrorCodes {
 		errorCodes.putAll(clientSessionCodes);
 	}
 
-	@Nullable
-	public static String getErrorDescription(@Nullable Integer errorCode) {
+	public static @Nullable String getErrorDescription(@Nullable Integer errorCode) {
 		return errorCode == null ? null : errorCodes.get(errorCode);
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/RegexFlags.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/RegexFlags.java
index 23c96f9e46..8b0f4b83ce 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/RegexFlags.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/RegexFlags.java
@@ -17,7 +17,7 @@
 
 import java.util.regex.Pattern;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /**
  * Utility to translate {@link Pattern#flags() regex flags} to MongoDB regex options and vice versa.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/SpringJsonWriter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/SpringJsonWriter.java
new file mode 100644
index 0000000000..07eab92a01
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/SpringJsonWriter.java
@@ -0,0 +1,487 @@
+/*
+ * 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.mongodb.util;
+
+import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME;
+
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Base64;
+
+import org.bson.BsonBinary;
+import org.bson.BsonDbPointer;
+import org.bson.BsonReader;
+import org.bson.BsonRegularExpression;
+import org.bson.BsonTimestamp;
+import org.bson.BsonWriter;
+import org.bson.types.Decimal128;
+import org.bson.types.ObjectId;
+import org.jspecify.annotations.NullUnmarked;
+import org.springframework.util.StringUtils;
+
+/**
+ * Internal {@link BsonWriter} implementation that allows to render {@link #writePlaceholder(String) placeholders} as
+ * {@code ?0}.
+ *
+ * @author Christoph Strobl
+ * @since 5.0
+ */
+@NullUnmarked
+class SpringJsonWriter implements BsonWriter {
+
+	private final StringBuffer buffer;
+
+	private enum JsonContextType {
+		TOP_LEVEL, DOCUMENT, ARRAY,
+	}
+
+	private enum State {
+		INITIAL, NAME, VALUE, DONE
+	}
+
+	private static class JsonContext {
+
+		private final JsonContext parentContext;
+		private final JsonContextType contextType;
+		private boolean hasElements;
+
+		JsonContext(final JsonContext parentContext, final JsonContextType contextType) {
+			this.parentContext = parentContext;
+			this.contextType = contextType;
+		}
+
+		JsonContext nestedDocument() {
+			return new JsonContext(this, JsonContextType.DOCUMENT);
+		}
+
+		JsonContext nestedArray() {
+			return new JsonContext(this, JsonContextType.ARRAY);
+		}
+	}
+
+	private JsonContext context = new JsonContext(null, JsonContextType.TOP_LEVEL);
+	private State state = State.INITIAL;
+
+	public SpringJsonWriter(StringBuffer buffer) {
+		this.buffer = buffer;
+	}
+
+	@Override
+	public void flush() {}
+
+	@Override
+	public void writeBinaryData(BsonBinary binary) {
+
+		preWriteValue();
+		writeStartDocument();
+
+		writeName("$binary");
+
+		writeStartDocument();
+		writeName("base64");
+		writeString(Base64.getEncoder().encodeToString(binary.getData()));
+		writeName("subType");
+		writeInt32(binary.getBsonType().getValue());
+		writeEndDocument();
+
+		writeEndDocument();
+	}
+
+	@Override
+	public void writeBinaryData(String name, BsonBinary binary) {
+
+		writeName(name);
+		writeBinaryData(binary);
+	}
+
+	@Override
+	public void writeBoolean(boolean value) {
+
+		preWriteValue();
+		write(value ? "true" : "false");
+		setNextState();
+	}
+
+	@Override
+	public void writeBoolean(String name, boolean value) {
+
+		writeName(name);
+		writeBoolean(value);
+	}
+
+	@Override
+	public void writeDateTime(long value) {
+
+		// "$date": "2018-11-10T22:26:12.111Z"
+		writeStartDocument();
+		writeName("$date");
+		writeString(ZonedDateTime.ofInstant(Instant.ofEpochMilli(value), ZoneId.of("Z")).format(ISO_OFFSET_DATE_TIME));
+		writeEndDocument();
+	}
+
+	@Override
+	public void writeDateTime(String name, long value) {
+
+		writeName(name);
+		writeDateTime(value);
+	}
+
+	@Override
+	public void writeDBPointer(BsonDbPointer value) {
+
+	}
+
+	@Override
+	public void writeDBPointer(String name, BsonDbPointer value) {
+
+	}
+
+	@Override // {"$numberDouble":"10.5"}
+	public void writeDouble(double value) {
+
+		writeStartDocument();
+		writeName("$numberDouble");
+		writeString(Double.valueOf(value).toString());
+		writeEndDocument();
+	}
+
+	@Override
+	public void writeDouble(String name, double value) {
+
+		writeName(name);
+		writeDouble(value);
+	}
+
+	@Override
+	public void writeEndArray() {
+		write("]");
+		context = context.parentContext;
+		if (context.contextType == JsonContextType.TOP_LEVEL) {
+			state = State.DONE;
+		} else {
+			setNextState();
+		}
+	}
+
+	@Override
+	public void writeEndDocument() {
+		buffer.append("}");
+		context = context.parentContext;
+		if (context.contextType == JsonContextType.TOP_LEVEL) {
+			state = State.DONE;
+		} else {
+			setNextState();
+		}
+	}
+
+	@Override
+	public void writeInt32(int value) {
+
+		writeStartDocument();
+		writeName("$numberInt");
+		writeString(Integer.valueOf(value).toString());
+		writeEndDocument();
+	}
+
+	@Override
+	public void writeInt32(String name, int value) {
+
+		writeName(name);
+		writeInt32(value);
+	}
+
+	@Override
+	public void writeInt64(long value) {
+
+		writeStartDocument();
+		writeName("$numberLong");
+		writeString(Long.valueOf(value).toString());
+		writeEndDocument();
+	}
+
+	@Override
+	public void writeInt64(String name, long value) {
+
+		writeName(name);
+		writeInt64(value);
+	}
+
+	@Override
+	public void writeDecimal128(Decimal128 value) {
+
+		// { "$numberDecimal": "<number>" }
+		writeStartDocument();
+		writeName("$numberDecimal");
+		writeString(value.toString());
+		writeEndDocument();
+	}
+
+	@Override
+	public void writeDecimal128(String name, Decimal128 value) {
+
+		writeName(name);
+		writeDecimal128(value);
+	}
+
+	@Override
+	public void writeJavaScript(String code) {
+
+		writeStartDocument();
+		writeName("$code");
+		writeString(code);
+		writeEndDocument();
+	}
+
+	@Override
+	public void writeJavaScript(String name, String code) {
+
+		writeName(name);
+		writeJavaScript(code);
+	}
+
+	@Override
+	public void writeJavaScriptWithScope(String code) {
+
+	}
+
+	@Override
+	public void writeJavaScriptWithScope(String name, String code) {
+
+	}
+
+	@Override
+	public void writeMaxKey() {
+
+		writeStartDocument();
+		writeName("$maxKey");
+		buffer.append(1);
+		writeEndDocument();
+	}
+
+	@Override
+	public void writeMaxKey(String name) {
+		writeName(name);
+		writeMaxKey();
+	}
+
+	@Override
+	public void writeMinKey() {
+
+		writeStartDocument();
+		writeName("$minKey");
+		buffer.append(1);
+		writeEndDocument();
+	}
+
+	@Override
+	public void writeMinKey(String name) {
+		writeName(name);
+		writeMinKey();
+	}
+
+	@Override
+	public void writeName(String name) {
+		if (context.hasElements) {
+			write(",");
+		} else {
+			context.hasElements = true;
+		}
+
+		writeString(name);
+		buffer.append(":");
+		state = State.VALUE;
+	}
+
+	@Override
+	public void writeNull() {
+		buffer.append("null");
+	}
+
+	@Override
+	public void writeNull(String name) {
+		writeName(name);
+		writeNull();
+	}
+
+	@Override
+	public void writeObjectId(ObjectId objectId) {
+		writeStartDocument();
+		writeName("$oid");
+		writeString(objectId.toHexString());
+		writeEndDocument();
+	}
+
+	@Override
+	public void writeObjectId(String name, ObjectId objectId) {
+		writeName(name);
+		writeObjectId(objectId);
+	}
+
+	@Override
+	public void writeRegularExpression(BsonRegularExpression regularExpression) {
+
+		writeStartDocument();
+		writeName("$regex");
+
+		write("/");
+		write(regularExpression.getPattern());
+		write("/");
+
+		if (StringUtils.hasText(regularExpression.getOptions())) {
+			writeName("$options");
+			writeString(regularExpression.getOptions());
+		}
+
+		writeEndDocument();
+	}
+
+	@Override
+	public void writeRegularExpression(String name, BsonRegularExpression regularExpression) {
+		writeName(name);
+		writeRegularExpression(regularExpression);
+	}
+
+	@Override
+	public void writeStartArray() {
+
+		preWriteValue();
+		write("[");
+		context = context.nestedArray();
+	}
+
+	@Override
+	public void writeStartArray(String name) {
+		writeName(name);
+		writeStartArray();
+	}
+
+	@Override
+	public void writeStartDocument() {
+
+		preWriteValue();
+		write("{");
+		context = context.nestedDocument();
+		state = State.NAME;
+	}
+
+	@Override
+	public void writeStartDocument(String name) {
+		writeName(name);
+		writeStartDocument();
+	}
+
+	@Override
+	public void writeString(String value) {
+		write("'");
+		write(value);
+		write("'");
+	}
+
+	@Override
+	public void writeString(String name, String value) {
+		writeName(name);
+		writeString(value);
+	}
+
+	@Override
+	public void writeSymbol(String value) {
+
+		writeStartDocument();
+		writeName("$symbol");
+		writeString(value);
+		writeEndDocument();
+	}
+
+	@Override
+	public void writeSymbol(String name, String value) {
+
+		writeName(name);
+		writeSymbol(value);
+	}
+
+	@Override // {"$timestamp": {"t": <t>, "i": <i>}}
+	public void writeTimestamp(BsonTimestamp value) {
+
+		preWriteValue();
+		writeStartDocument();
+		writeName("$timestamp");
+		writeStartDocument();
+		writeName("t");
+		buffer.append(value.getTime());
+		writeName("i");
+		buffer.append(value.getInc());
+		writeEndDocument();
+		writeEndDocument();
+	}
+
+	@Override
+	public void writeTimestamp(String name, BsonTimestamp value) {
+
+		writeName(name);
+		writeTimestamp(value);
+	}
+
+	@Override
+	public void writeUndefined() {
+
+		writeStartDocument();
+		writeName("$undefined");
+		writeBoolean(true);
+		writeEndDocument();
+	}
+
+	@Override
+	public void writeUndefined(String name) {
+
+		writeName(name);
+		writeUndefined();
+	}
+
+	@Override
+	public void pipe(BsonReader reader) {
+
+	}
+
+	/**
+	 * @param placeholder
+	 */
+	public void writePlaceholder(String placeholder) {
+		write(placeholder);
+	}
+
+	private void write(String str) {
+		buffer.append(str);
+	}
+
+	private void preWriteValue() {
+
+		if (context.contextType == JsonContextType.ARRAY) {
+			if (context.hasElements) {
+				write(",");
+			}
+		}
+		context.hasElements = true;
+	}
+
+	private void setNextState() {
+		if (context.contextType == JsonContextType.ARRAY) {
+			state = State.VALUE;
+		} else {
+			state = State.NAME;
+		}
+	}
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/aggregation/TestAggregationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/aggregation/TestAggregationContext.java
index 344244717e..950f9ec797 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/aggregation/TestAggregationContext.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/aggregation/TestAggregationContext.java
@@ -17,6 +17,7 @@
 
 import org.bson.Document;
 import org.bson.codecs.configuration.CodecRegistry;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.aggregation.Aggregation;
 import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext;
 import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
@@ -27,7 +28,6 @@
 import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
 import org.springframework.data.mongodb.core.convert.QueryMapper;
 import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
-import org.springframework.lang.Nullable;
 
 /**
  * @author Christoph Strobl
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/encryption/EncryptionUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/encryption/EncryptionUtils.java
index 9dd3f1d8fb..be9a2e1cff 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/encryption/EncryptionUtils.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/encryption/EncryptionUtils.java
@@ -22,10 +22,10 @@
 import org.bson.BsonBinary;
 import org.bson.BsonBinarySubType;
 import org.bson.types.Binary;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.util.spel.ExpressionUtils;
 import org.springframework.expression.EvaluationContext;
 import org.springframework.expression.Expression;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
 /**
@@ -48,8 +48,7 @@ public final class EncryptionUtils {
 	 * @return can be {@literal null}.
 	 * @throws IllegalArgumentException if one of the required arguments is {@literal null}.
 	 */
-	@Nullable
-	public static Object resolveKeyId(String value, Supplier<EvaluationContext> evaluationContext) {
+	public static @Nullable Object resolveKeyId(String value, Supplier<EvaluationContext> evaluationContext) {
 
 		Assert.notNull(value, "Value must not be null");
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/DateTimeFormatter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/DateTimeFormatter.java
index b5c26755cf..3961fafc21 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/DateTimeFormatter.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/DateTimeFormatter.java
@@ -23,6 +23,8 @@
 import java.time.ZoneOffset;
 import java.time.ZonedDateTime;
 
+import org.jspecify.annotations.NullUnmarked;
+
 /**
  * DateTimeFormatter implementation borrowed from <a href=
  * "https://github.com/mongodb/mongo-java-driver/blob/master/bson/src/main/org/bson/json/DateTimeFormatter.java">MongoDB
@@ -33,6 +35,7 @@
  * @author Ross Lawley
  * @since 2.2
  */
+@NullUnmarked
 class DateTimeFormatter {
 
 	private static final int DATE_STRING_LENGTH = "1970-01-01".length();
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/EvaluationContextExpressionEvaluator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/EvaluationContextExpressionEvaluator.java
index 6c31a9721f..57fecd284c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/EvaluationContextExpressionEvaluator.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/EvaluationContextExpressionEvaluator.java
@@ -18,12 +18,12 @@
 import java.util.Collections;
 import java.util.Map;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mapping.model.ValueExpressionEvaluator;
 import org.springframework.expression.EvaluationContext;
 import org.springframework.expression.Expression;
 import org.springframework.expression.ExpressionParser;
 import org.springframework.expression.spel.support.StandardEvaluationContext;
-import org.springframework.lang.Nullable;
 
 /**
  * @author Christoph Strobl
@@ -40,9 +40,8 @@ class EvaluationContextExpressionEvaluator implements ValueExpressionEvaluator {
 		this.expressionParser = expressionParser;
 	}
 
-	@Nullable
 	@Override
-	public <T> T evaluate(String expression) {
+	public <T> @Nullable T evaluate(String expression) {
 		return evaluateExpression(expression, Collections.emptyMap());
 	}
 
@@ -55,7 +54,7 @@ Expression getParsedExpression(String expressionString) {
 	}
 
 	@SuppressWarnings("unchecked")
-	<T> T evaluateExpression(String expressionString, Map<String, Object> variables) {
+	<T> @Nullable T evaluateExpression(String expressionString, Map<String, Object> variables) {
 
 		Expression expression = getParsedExpression(expressionString);
 		EvaluationContext ctx = getEvaluationContext(expressionString);
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonBuffer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonBuffer.java
index 4b4b497dae..dcb9a3ff13 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonBuffer.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonBuffer.java
@@ -16,6 +16,7 @@
 package org.springframework.data.mongodb.util.json;
 
 import org.bson.json.JsonParseException;
+import org.jspecify.annotations.NullUnmarked;
 
 /**
  * JsonBuffer implementation borrowed from <a href=
@@ -27,6 +28,7 @@
  * @author Ross Lawley
  * @since 2.2
  */
+@NullUnmarked
 class JsonBuffer {
 
 	private final String buffer;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonScanner.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonScanner.java
index ca4fbddd60..461e52489f 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonScanner.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonScanner.java
@@ -17,6 +17,7 @@
 
 import org.bson.BsonRegularExpression;
 import org.bson.json.JsonParseException;
+import org.jspecify.annotations.NullUnmarked;
 
 /**
  * Parses the string representation of a JSON object into a set of {@link JsonToken}-derived objects. <br />
@@ -32,6 +33,7 @@
  * @author Christoph Strobl
  * @since 2.2
  */
+@NullUnmarked
 class JsonScanner {
 
 	private final JsonBuffer buffer;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonToken.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonToken.java
index 293736123e..e73d57774b 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonToken.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonToken.java
@@ -20,6 +20,7 @@
 import org.bson.BsonDouble;
 import org.bson.json.JsonParseException;
 import org.bson.types.Decimal128;
+import org.jspecify.annotations.NullUnmarked;
 
 /**
  * JsonToken implementation borrowed from <a href=
@@ -30,6 +31,7 @@
  * @author Ross Lawley
  * @since 2.2
  */
+@NullUnmarked
 class JsonToken {
 
 	private final Object value;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingContext.java
index b4fd13b3af..55dccad0c9 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingContext.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingContext.java
@@ -19,7 +19,7 @@
 import java.util.function.Function;
 import java.util.function.Supplier;
 
-import org.springframework.data.mapping.model.SpELExpressionEvaluator;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mapping.model.ValueExpressionEvaluator;
 import org.springframework.data.spel.ExpressionDependencies;
 import org.springframework.data.util.Lazy;
@@ -28,8 +28,6 @@
 import org.springframework.expression.ExpressionParser;
 import org.springframework.expression.ParseException;
 import org.springframework.expression.ParserContext;
-import org.springframework.expression.spel.standard.SpelExpressionParser;
-import org.springframework.lang.Nullable;
 
 /**
  * Reusable context for binding parameters to a placeholder or a SpEL expression within a JSON structure. <br />
@@ -44,29 +42,6 @@ public class ParameterBindingContext {
 	private final ValueProvider valueProvider;
 	private final ValueExpressionEvaluator expressionEvaluator;
 
-	/**
-	 * @param valueProvider
-	 * @param expressionParser
-	 * @param evaluationContext
-	 * @deprecated since 4.4.0, use {@link #ParameterBindingContext(ValueProvider, ExpressionParser, Supplier)} instead.
-	 */
-	@Deprecated(since = "4.4.0")
-	public ParameterBindingContext(ValueProvider valueProvider, SpelExpressionParser expressionParser,
-			EvaluationContext evaluationContext) {
-		this(valueProvider, expressionParser, () -> evaluationContext);
-	}
-
-	/**
-	 * @param valueProvider
-	 * @param expressionEvaluator
-	 * @since 3.1
-	 * @deprecated since 4.4.0, use {@link #ParameterBindingContext(ValueProvider, ValueExpressionEvaluator)} instead.
-	 */
-	@Deprecated(since = "4.4.0")
-	public ParameterBindingContext(ValueProvider valueProvider, SpELExpressionEvaluator expressionEvaluator) {
-		this(valueProvider, (ValueExpressionEvaluator) expressionEvaluator);
-	}
-
 	/**
 	 * @param valueProvider
 	 * @param expressionParser
@@ -153,18 +128,15 @@ public static ParameterBindingContext forExpressions(ValueProvider valueProvider
 		return new ParameterBindingContext(valueProvider, expressionEvaluator);
 	}
 
-	@Nullable
-	public Object bindableValueForIndex(int index) {
+	public @Nullable Object bindableValueForIndex(int index) {
 		return valueProvider.getBindableValue(index);
 	}
 
-	@Nullable
-	public Object evaluateExpression(String expressionString) {
+	public @Nullable Object evaluateExpression(String expressionString) {
 		return expressionEvaluator.evaluate(expressionString);
 	}
 
-	@Nullable
-	public Object evaluateExpression(String expressionString, Map<String, Object> variables) {
+	public @Nullable Object evaluateExpression(String expressionString, Map<String, Object> variables) {
 
 		if (expressionEvaluator instanceof EvaluationContextExpressionEvaluator expressionEvaluator) {
 			return expressionEvaluator.evaluateExpression(expressionString, variables);
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java
index ffa226ab69..8138f397a6 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java
@@ -40,14 +40,14 @@
 import org.bson.codecs.*;
 import org.bson.codecs.configuration.CodecRegistry;
 import org.bson.json.JsonParseException;
-
+import org.jspecify.annotations.NullUnmarked;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.expression.ValueExpressionParser;
 import org.springframework.data.mapping.model.ValueExpressionEvaluator;
 import org.springframework.data.mongodb.core.mapping.FieldName;
 import org.springframework.data.spel.EvaluationContextProvider;
 import org.springframework.data.spel.ExpressionDependencies;
 import org.springframework.expression.spel.standard.SpelExpressionParser;
-import org.springframework.lang.Nullable;
 import org.springframework.util.NumberUtils;
 import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
@@ -66,6 +66,7 @@
  * @author Rocco Lagrotteria
  * @since 2.2
  */
+@NullUnmarked
 public class ParameterBindingDocumentCodec implements CollectibleCodec<Document> {
 
 	private static final String ID_FIELD_NAME = FieldName.ID.name();
@@ -170,7 +171,7 @@ public void encode(final BsonWriter writer, final Document document, final Encod
 	public Document decode(@Nullable String json, Object[] values) {
 
 		return decode(json, new ParameterBindingContext((index) -> values[index], new SpelExpressionParser(),
-				EvaluationContextProvider.DEFAULT.getEvaluationContext(values)));
+				() -> EvaluationContextProvider.DEFAULT.getEvaluationContext(values)));
 	}
 
 	public Document decode(@Nullable String json, ParameterBindingContext bindingContext) {
@@ -396,9 +397,8 @@ static class DependencyCapturingExpressionEvaluator implements ValueExpressionEv
 			this.expressionParser = expressionParser;
 		}
 
-		@Nullable
 		@Override
-		public <T> T evaluate(String expression) {
+		public <T> @Nullable T evaluate(String expression) {
 
 			dependencies.add(expressionParser.parse(expression).getExpressionDependencies());
 			return (T) PLACEHOLDER;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java
index 8dd42e2427..c1e519e2f5 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java
@@ -39,10 +39,11 @@
 import org.bson.types.MaxKey;
 import org.bson.types.MinKey;
 import org.bson.types.ObjectId;
+import org.jspecify.annotations.NullUnmarked;
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.spel.EvaluationContextProvider;
 import org.springframework.expression.EvaluationContext;
 import org.springframework.expression.spel.standard.SpelExpressionParser;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.NumberUtils;
 import org.springframework.util.ObjectUtils;
@@ -62,6 +63,7 @@
  * @author Rocco Lagrotteria
  * @since 2.2
  */
+@NullUnmarked
 public class ParameterBindingJsonReader extends AbstractBsonReader {
 
 	private static final Pattern ENTIRE_QUERY_BINDING_PATTERN = Pattern.compile("^\\?(\\d+)$|^[\\?:][#$]\\{.*\\}$");
@@ -533,13 +535,11 @@ private BsonType bsonTypeForValue(Object value) {
 		return BsonType.UNDEFINED;
 	}
 
-	@Nullable
-	private Object evaluateExpression(String expressionString) {
+	private @Nullable Object evaluateExpression(String expressionString) {
 		return bindingContext.evaluateExpression(expressionString, Collections.emptyMap());
 	}
 
-	@Nullable
-	private Object evaluateExpression(String expressionString, Map<String,Object> variables) {
+	private @Nullable Object evaluateExpression(String expressionString, Map<String,Object> variables) {
 		return bindingContext.evaluateExpression(expressionString, variables);
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ValueProvider.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ValueProvider.java
index 8f1d23885d..2ce22214fb 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ValueProvider.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ValueProvider.java
@@ -15,7 +15,7 @@
  */
 package org.springframework.data.mongodb.util.json;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /**
  * A value provider to retrieve bindable values by their parameter index.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/package-info.java
index 8a86b3522b..60e5e8c609 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/package-info.java
@@ -1,5 +1,5 @@
 /**
  * MongoDB driver-specific utility classes for Json conversion.
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 package org.springframework.data.mongodb.util.json;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/package-info.java
index 7caec410f5..a697bb7000 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/package-info.java
@@ -2,5 +2,5 @@
  * MongoDB driver-specific utility classes for {@link org.bson.conversions.Bson} and {@link com.mongodb.DBObject}
  * interaction.
  */
-@org.springframework.lang.NonNullApi
+@org.jspecify.annotations.NullMarked
 package org.springframework.data.mongodb.util;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/spel/ExpressionUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/spel/ExpressionUtils.java
index 9fa66b3b2b..796f618906 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/spel/ExpressionUtils.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/spel/ExpressionUtils.java
@@ -17,12 +17,12 @@
 
 import java.util.function.Supplier;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.expression.EvaluationContext;
 import org.springframework.expression.Expression;
 import org.springframework.expression.ParserContext;
 import org.springframework.expression.common.LiteralExpression;
 import org.springframework.expression.spel.standard.SpelExpressionParser;
-import org.springframework.lang.Nullable;
 import org.springframework.util.StringUtils;
 
 /**
@@ -42,8 +42,7 @@ public final class ExpressionUtils {
 	 * @param potentialExpression can be {@literal null}
 	 * @return can be {@literal null}.
 	 */
-	@Nullable
-	public static Expression detectExpression(@Nullable String potentialExpression) {
+	public static @Nullable Expression detectExpression(@Nullable String potentialExpression) {
 
 		if (!StringUtils.hasText(potentialExpression)) {
 			return null;
@@ -53,8 +52,7 @@ public static Expression detectExpression(@Nullable String potentialExpression)
 		return expression instanceof LiteralExpression ? null : expression;
 	}
 
-	@Nullable
-	public static Object evaluate(String value, Supplier<EvaluationContext> evaluationContext) {
+	public static @Nullable Object evaluate(String value, Supplier<EvaluationContext> evaluationContext) {
 
 		Expression expression = detectExpression(value);
 		if (expression == null) {
diff --git a/spring-data-mongodb/src/main/kotlin/org/springframework/data/mongodb/core/query/TypedUpdateExtensions.kt b/spring-data-mongodb/src/main/kotlin/org/springframework/data/mongodb/core/query/TypedUpdateExtensions.kt
index d132482f65..d7784a7768 100644
--- a/spring-data-mongodb/src/main/kotlin/org/springframework/data/mongodb/core/query/TypedUpdateExtensions.kt
+++ b/spring-data-mongodb/src/main/kotlin/org/springframework/data/mongodb/core/query/TypedUpdateExtensions.kt
@@ -142,7 +142,7 @@ fun <T> Update.pull(key: KProperty<T>, value: Any) =
  * @since 4.4
  * @see Update.pullAll
  */
-fun <T> Update.pullAll(key: KProperty<Collection<T>>, values: Array<T>) =
+fun <T> Update.pullAll(key: KProperty<Collection<T>>, values: Array<Any>) =
     pullAll(key.toDotPath(), values)
 
 /**
diff --git a/spring-data-mongodb/src/main/resources/META-INF/spring.schemas b/spring-data-mongodb/src/main/resources/META-INF/spring.schemas
index 57920f7449..c6c28dbab1 100644
--- a/spring-data-mongodb/src/main/resources/META-INF/spring.schemas
+++ b/spring-data-mongodb/src/main/resources/META-INF/spring.schemas
@@ -13,7 +13,8 @@ http\://www.springframework.org/schema/data/mongo/spring-mongo-2.2.xsd=org/sprin
 http\://www.springframework.org/schema/data/mongo/spring-mongo-3.0.xsd=org/springframework/data/mongodb/config/spring-mongo-3.0.xsd
 http\://www.springframework.org/schema/data/mongo/spring-mongo-3.3.xsd=org/springframework/data/mongodb/config/spring-mongo-3.3.xsd
 http\://www.springframework.org/schema/data/mongo/spring-mongo-4.0.xsd=org/springframework/data/mongodb/config/spring-mongo-4.0.xsd
-http\://www.springframework.org/schema/data/mongo/spring-mongo.xsd=org/springframework/data/mongodb/config/spring-mongo-4.0.xsd
+http\://www.springframework.org/schema/data/mongo/spring-mongo-5.0.xsd=org/springframework/data/mongodb/config/spring-mongo-5.0.xsd
+http\://www.springframework.org/schema/data/mongo/spring-mongo.xsd=org/springframework/data/mongodb/config/spring-mongo-5.0.xsd
 https\://www.springframework.org/schema/data/mongo/spring-mongo-1.0.xsd=org/springframework/data/mongodb/config/spring-mongo-1.0.xsd
 https\://www.springframework.org/schema/data/mongo/spring-mongo-1.1.xsd=org/springframework/data/mongodb/config/spring-mongo-1.1.xsd
 https\://www.springframework.org/schema/data/mongo/spring-mongo-1.2.xsd=org/springframework/data/mongodb/config/spring-mongo-1.2.xsd
@@ -29,4 +30,5 @@ https\://www.springframework.org/schema/data/mongo/spring-mongo-2.2.xsd=org/spri
 https\://www.springframework.org/schema/data/mongo/spring-mongo-3.0.xsd=org/springframework/data/mongodb/config/spring-mongo-3.0.xsd
 https\://www.springframework.org/schema/data/mongo/spring-mongo-3.3.xsd=org/springframework/data/mongodb/config/spring-mongo-3.3.xsd
 https\://www.springframework.org/schema/data/mongo/spring-mongo-4.0.xsd=org/springframework/data/mongodb/config/spring-mongo-4.0.xsd
-https\://www.springframework.org/schema/data/mongo/spring-mongo.xsd=org/springframework/data/mongodb/config/spring-mongo-4.0.xsd
+https\://www.springframework.org/schema/data/mongo/spring-mongo-5.0.xsd=org/springframework/data/mongodb/config/spring-mongo-5.0.xsd
+https\://www.springframework.org/schema/data/mongo/spring-mongo.xsd=org/springframework/data/mongodb/config/spring-mongo-5.0.xsd
diff --git a/spring-data-mongodb/src/main/resources/org/springframework/data/mongodb/config/spring-mongo-5.0.xsd b/spring-data-mongodb/src/main/resources/org/springframework/data/mongodb/config/spring-mongo-5.0.xsd
new file mode 100644
index 0000000000..5fae630b6b
--- /dev/null
+++ b/spring-data-mongodb/src/main/resources/org/springframework/data/mongodb/config/spring-mongo-5.0.xsd
@@ -0,0 +1,935 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+  ~ Copyright 2019-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.
+  -->
+<xsd:schema xmlns="http://www.springframework.org/schema/data/mongo"
+            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+            xmlns:beans="http://www.springframework.org/schema/beans"
+            xmlns:tool="http://www.springframework.org/schema/tool"
+            xmlns:repository="http://www.springframework.org/schema/data/repository"
+            targetNamespace="http://www.springframework.org/schema/data/mongo"
+            elementFormDefault="qualified" attributeFormDefault="unqualified">
+
+	<xsd:import namespace="http://www.springframework.org/schema/beans"/>
+	<xsd:import namespace="http://www.springframework.org/schema/tool"/>
+	<xsd:import namespace="http://www.springframework.org/schema/context"/>
+	<xsd:import namespace="http://www.springframework.org/schema/data/repository"
+	            schemaLocation="https://www.springframework.org/schema/data/repository/spring-repository.xsd"/>
+
+	<xsd:element name="mongo-client" type="mongoClientType">
+		<xsd:annotation>
+			<xsd:documentation
+					source="org.springframework.data.mongodb.core.MongoClientFactoryBean">
+				<![CDATA[
+Defines a com.mongodb.client.MongoClient instance used for accessing MongoDB.
+			]]></xsd:documentation>
+			<xsd:appinfo>
+				<tool:annotation>
+					<tool:exports type="com.mongodb.client.MongoClient"/>
+				</tool:annotation>
+			</xsd:appinfo>
+		</xsd:annotation>
+	</xsd:element>
+
+	<xsd:element name="db-factory">
+		<xsd:annotation>
+			<xsd:documentation><![CDATA[
+Defines a MongoDbFactory for connecting to a specific database
+			]]></xsd:documentation>
+		</xsd:annotation>
+		<xsd:complexType>
+			<xsd:attribute name="id" type="xsd:string" use="optional">
+				<xsd:annotation>
+					<xsd:documentation><![CDATA[
+The name of the MongoDatabaseFactory definition (by default "mongoDbFactory").]]></xsd:documentation>
+				</xsd:annotation>
+			</xsd:attribute>
+			<xsd:attribute name="mongo-client-ref" type="mongoRef" use="optional">
+				<xsd:annotation>
+					<xsd:documentation><![CDATA[
+The reference to a com.mongodb.client.MongoClient instance.
+					]]>
+					</xsd:documentation>
+				</xsd:annotation>
+			</xsd:attribute>
+			<xsd:attribute name="dbname" type="xsd:string" use="optional">
+				<xsd:annotation>
+					<xsd:documentation><![CDATA[
+The name of the database to connect to. Default is 'db'.
+							]]></xsd:documentation>
+				</xsd:annotation>
+			</xsd:attribute>
+			<xsd:attribute name="client-uri" type="xsd:string" use="optional">
+				<xsd:annotation>
+					<xsd:documentation><![CDATA[
+The MongoClientURI string.
+@Deprecated since 3.0 - Use connection-string instead.
+                    ]]>
+					</xsd:documentation>
+				</xsd:annotation>
+			</xsd:attribute>
+			<xsd:attribute name="connection-string" type="xsd:string" use="optional">
+				<xsd:annotation>
+					<xsd:documentation><![CDATA[
+The MongoDB Connection String. Supersedes client-uri.
+See https://docs.mongodb.com/manual/reference/connection-string/ for full documentation.
+@Since 3.0
+                    ]]>
+					</xsd:documentation>
+				</xsd:annotation>
+			</xsd:attribute>
+			<xsd:attribute name="write-concern">
+				<xsd:annotation>
+					<xsd:documentation>
+						The WriteConcern that will be the default value used when asking
+						the MongoDatabaseFactory for a DB object
+					</xsd:documentation>
+				</xsd:annotation>
+				<xsd:simpleType>
+					<xsd:union memberTypes="writeConcernEnumeration xsd:string"/>
+				</xsd:simpleType>
+			</xsd:attribute>
+		</xsd:complexType>
+	</xsd:element>
+
+	<xsd:attributeGroup name="mongo-repository-attributes">
+		<xsd:attribute name="mongo-template-ref" type="mongoTemplateRef"
+		               default="mongoTemplate">
+			<xsd:annotation>
+				<xsd:documentation>
+					The reference to a MongoTemplate. Will default to 'mongoTemplate'.
+				</xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="create-query-indexes" default="false">
+			<xsd:annotation>
+				<xsd:documentation>
+					Enables creation of indexes for queries that get derived from the
+					method name
+					and thus reference domain class properties. Defaults to false.
+				</xsd:documentation>
+			</xsd:annotation>
+			<xsd:simpleType>
+				<xsd:union memberTypes="booleanType xsd:string"/>
+			</xsd:simpleType>
+		</xsd:attribute>
+	</xsd:attributeGroup>
+
+	<xsd:element name="repositories">
+		<xsd:complexType>
+			<xsd:complexContent>
+				<xsd:extension base="repository:repositories">
+					<xsd:attributeGroup ref="mongo-repository-attributes"/>
+					<xsd:attributeGroup ref="repository:repository-attributes"/>
+				</xsd:extension>
+			</xsd:complexContent>
+		</xsd:complexType>
+	</xsd:element>
+
+	<xsd:element name="mapping-converter">
+		<xsd:annotation>
+			<xsd:documentation>
+				<![CDATA[Defines a MongoConverter for getting rich mapping functionality.]]></xsd:documentation>
+			<xsd:appinfo>
+				<tool:exports
+						type="org.springframework.data.mongodb.core.convert.MappingMongoConverter"/>
+			</xsd:appinfo>
+		</xsd:annotation>
+		<xsd:complexType>
+			<xsd:sequence>
+				<xsd:element name="custom-converters" minOccurs="0">
+					<xsd:annotation>
+						<xsd:documentation><![CDATA[
+		Top-level element that contains one or more custom converters to be used for mapping
+		domain objects to and from Mongo's Document]]>
+						</xsd:documentation>
+					</xsd:annotation>
+					<xsd:complexType>
+						<xsd:sequence>
+							<xsd:element name="converter" type="customConverterType"
+							             minOccurs="0" maxOccurs="unbounded"/>
+						</xsd:sequence>
+						<xsd:attribute name="base-package" type="xsd:string"/>
+					</xsd:complexType>
+				</xsd:element>
+			</xsd:sequence>
+			<xsd:attribute name="id" type="xsd:string" use="optional">
+				<xsd:annotation>
+					<xsd:documentation><![CDATA[
+The name of the MappingMongoConverter instance (by default "mappingConverter").]]></xsd:documentation>
+				</xsd:annotation>
+			</xsd:attribute>
+			<xsd:attribute name="base-package" type="xsd:string" use="optional">
+				<xsd:annotation>
+					<xsd:documentation><![CDATA[
+The base package in which to scan for entities annotated with @Document
+							]]></xsd:documentation>
+				</xsd:annotation>
+			</xsd:attribute>
+			<xsd:attribute name="db-factory-ref" type="xsd:string" use="optional">
+				<xsd:annotation>
+					<xsd:documentation>
+						The reference to a MongoDatabaseFactory.
+					</xsd:documentation>
+					<xsd:appinfo>
+						<tool:annotation kind="ref">
+							<tool:assignable-to
+									type="org.springframework.data.mongodb.MongoDatabaseFactory"/>
+						</tool:annotation>
+					</xsd:appinfo>
+				</xsd:annotation>
+			</xsd:attribute>
+			<xsd:attribute name="type-mapper-ref" type="typeMapperRef" use="optional">
+				<xsd:annotation>
+					<xsd:documentation>
+						The reference to a MongoTypeMapper to be used by this
+						MappingMongoConverter.
+					</xsd:documentation>
+				</xsd:annotation>
+			</xsd:attribute>
+			<xsd:attribute name="mapping-context-ref" type="mappingContextRef"
+			               use="optional">
+				<xsd:annotation>
+					<xsd:documentation
+							source="org.springframework.data.mapping.model.MappingContext">
+						The reference to a MappingContext. Will default to
+						'mappingContext'.
+					</xsd:documentation>
+				</xsd:annotation>
+			</xsd:attribute>
+			<xsd:attribute name="disable-validation" use="optional">
+				<xsd:annotation>
+					<xsd:documentation
+							source="org.springframework.data.mongodb.core.mapping.event.ValidatingMongoEventListener">
+						Disables JSR-303 validation on MongoDB documents before they are
+						saved. By default it is set to false.
+					</xsd:documentation>
+				</xsd:annotation>
+				<xsd:simpleType>
+					<xsd:union memberTypes="xsd:boolean xsd:string"/>
+				</xsd:simpleType>
+			</xsd:attribute>
+			<xsd:attribute name="abbreviate-field-names" use="optional">
+				<xsd:annotation>
+					<xsd:documentation
+							source="org.springframework.data.mongodb.core.mapping.CamelCaseAbbreviatingFieldNamingStrategy">
+						Enables abbreviating the field names for domain class properties
+						to the
+						first character of their camel case names, e.g. fooBar -> fb.
+						Defaults to false.
+					</xsd:documentation>
+				</xsd:annotation>
+				<xsd:simpleType>
+					<xsd:union memberTypes="xsd:boolean xsd:string"/>
+				</xsd:simpleType>
+			</xsd:attribute>
+			<xsd:attribute name="field-naming-strategy-ref" type="fieldNamingStrategyRef"
+			               use="optional">
+				<xsd:annotation>
+					<xsd:documentation
+							source="org.springframework.data.mongodb.core.mapping.FieldNamingStrategy">
+						The reference to a FieldNamingStrategy.
+					</xsd:documentation>
+				</xsd:annotation>
+			</xsd:attribute>
+			<xsd:attribute name="auto-index-creation" use="optional">
+				<xsd:annotation>
+					<xsd:documentation>
+						Enable/Disable index creation for annotated properties/entities.
+					</xsd:documentation>
+				</xsd:annotation>
+				<xsd:simpleType>
+					<xsd:union memberTypes="xsd:boolean xsd:string"/>
+				</xsd:simpleType>
+			</xsd:attribute>
+		</xsd:complexType>
+	</xsd:element>
+
+	<xsd:element name="auditing">
+		<xsd:annotation>
+			<xsd:appinfo>
+				<tool:annotation>
+					<tool:exports
+							type="org.springframework.data.mongodb.core.mapping.event.AuditingEntityCallback"/>
+					<tool:exports
+							type="org.springframework.data.auditing.IsNewAwareAuditingHandler"/>
+				</tool:annotation>
+			</xsd:appinfo>
+		</xsd:annotation>
+		<xsd:complexType>
+			<xsd:attributeGroup ref="repository:auditing-attributes"/>
+			<xsd:attribute name="mapping-context-ref" type="mappingContextRef"/>
+			<xsd:attribute name="mongo-converter-ref" type="mongoConverterRef"/>
+		</xsd:complexType>
+	</xsd:element>
+
+	<xsd:simpleType name="typeMapperRef">
+		<xsd:annotation>
+			<xsd:appinfo>
+				<tool:annotation kind="ref">
+					<tool:assignable-to
+							type="org.springframework.data.mongodb.core.convert.MongoTypeMapper"/>
+				</tool:annotation>
+			</xsd:appinfo>
+		</xsd:annotation>
+		<xsd:union memberTypes="xsd:string"/>
+	</xsd:simpleType>
+
+	<xsd:simpleType name="mappingContextRef">
+		<xsd:annotation>
+			<xsd:appinfo>
+				<tool:annotation kind="ref">
+					<tool:assignable-to
+							type="org.springframework.data.mapping.model.MappingContext"/>
+				</tool:annotation>
+			</xsd:appinfo>
+		</xsd:annotation>
+		<xsd:union memberTypes="xsd:string"/>
+	</xsd:simpleType>
+
+	<xsd:simpleType name="mongoConverterRef">
+		<xsd:annotation>
+			<xsd:appinfo>
+				<tool:annotation kind="ref">
+					<tool:assignable-to
+							type="org.springframework.data.mongodb.core.convert.MongoConverter"/>
+				</tool:annotation>
+			</xsd:appinfo>
+		</xsd:annotation>
+		<xsd:union memberTypes="xsd:string"/>
+	</xsd:simpleType>
+
+	<xsd:simpleType name="fieldNamingStrategyRef">
+		<xsd:annotation>
+			<xsd:appinfo>
+				<tool:annotation kind="ref">
+					<tool:assignable-to
+							type="org.springframework.data.mongodb.core.mapping.FieldNamingStrategy"/>
+				</tool:annotation>
+			</xsd:appinfo>
+		</xsd:annotation>
+		<xsd:union memberTypes="xsd:string"/>
+	</xsd:simpleType>
+
+	<xsd:simpleType name="mongoTemplateRef">
+		<xsd:annotation>
+			<xsd:appinfo>
+				<tool:annotation kind="ref">
+					<tool:assignable-to
+							type="org.springframework.data.mongodb.core.MongoTemplate"/>
+				</tool:annotation>
+			</xsd:appinfo>
+		</xsd:annotation>
+		<xsd:union memberTypes="xsd:string"/>
+	</xsd:simpleType>
+
+	<xsd:simpleType name="mongoRef">
+		<xsd:annotation>
+			<xsd:appinfo>
+				<tool:annotation kind="ref">
+					<tool:assignable-to
+							type="org.springframework.data.mongodb.core.MongoClientFactoryBean"/>
+				</tool:annotation>
+			</xsd:appinfo>
+		</xsd:annotation>
+		<xsd:union memberTypes="xsd:string"/>
+	</xsd:simpleType>
+
+	<xsd:simpleType name="sslSocketFactoryRef">
+		<xsd:annotation>
+			<xsd:appinfo>
+				<tool:annotation kind="ref">
+					<tool:assignable-to type="javax.net.ssl.SSLSocketFactory"/>
+				</tool:annotation>
+			</xsd:appinfo>
+		</xsd:annotation>
+		<xsd:union memberTypes="xsd:string"/>
+	</xsd:simpleType>
+
+	<xsd:simpleType name="encryptionSettingsRef">
+		<xsd:annotation>
+			<xsd:documentation><![CDATA[
+Reference to FactoryBean for com.mongodb.AutoEncryptionSettings - @since 2.2
+			]]></xsd:documentation>
+			<xsd:appinfo>
+				<tool:annotation kind="ref">
+					<tool:assignable-to
+							type="org.springframework.data.mongodb.core.MongoEncryptionSettingsFactoryBean"/>
+				</tool:annotation>
+			</xsd:appinfo>
+		</xsd:annotation>
+		<xsd:union memberTypes="xsd:string"/>
+	</xsd:simpleType>
+
+	<xsd:simpleType name="serverApiRef">
+		<xsd:annotation>
+			<xsd:documentation><![CDATA[
+Reference to FactoryBean for com.mongodb.MongoServerApiFactoryBean - @since 3.3
+			]]></xsd:documentation>
+			<xsd:appinfo>
+				<tool:annotation kind="ref">
+					<tool:assignable-to
+							type="org.springframework.data.mongodb.core.MongoServerApiFactoryBean"/>
+				</tool:annotation>
+			</xsd:appinfo>
+		</xsd:annotation>
+		<xsd:union memberTypes="xsd:string"/>
+	</xsd:simpleType>
+
+	<xsd:simpleType name="readConcernEnumeration">
+		<xsd:restriction base="xsd:token">
+			<xsd:enumeration value="DEFAULT"/>
+			<xsd:enumeration value="LOCAL"/>
+			<xsd:enumeration value="MAJORITY"/>
+			<xsd:enumeration value="LINEARIZABLE"/>
+			<xsd:enumeration value="AVAILABLE"/>
+		</xsd:restriction>
+	</xsd:simpleType>
+
+	<xsd:simpleType name="writeConcernEnumeration">
+		<xsd:restriction base="xsd:token">
+			<xsd:enumeration value="ACKNOWLEDGED"/>
+			<xsd:enumeration value="W1"/>
+			<xsd:enumeration value="W2"/>
+			<xsd:enumeration value="W3"/>
+			<xsd:enumeration value="UNACKNOWLEDGED"/>
+			<xsd:enumeration value="JOURNALED"/>
+			<xsd:enumeration value="MAJORITY"/>
+		</xsd:restriction>
+	</xsd:simpleType>
+
+	<xsd:simpleType name="readPreferenceEnumeration">
+		<xsd:restriction base="xsd:token">
+			<xsd:enumeration value="PRIMARY"/>
+			<xsd:enumeration value="PRIMARY_PREFERRED"/>
+			<xsd:enumeration value="SECONDARY"/>
+			<xsd:enumeration value="SECONDARY_PREFERRED"/>
+			<xsd:enumeration value="NEAREST"/>
+		</xsd:restriction>
+	</xsd:simpleType>
+
+	<xsd:simpleType name="uuidRepresentationEnumeration">
+		<xsd:restriction base="xsd:token">
+			<xsd:enumeration value="UNSPECIFIED"/>
+			<xsd:enumeration value="STANDARD"/>
+			<xsd:enumeration value="C_SHARP_LEGACY"/>
+			<xsd:enumeration value="JAVA_LEGACY"/>
+			<xsd:enumeration value="PYTHON_LEGACY"/>
+		</xsd:restriction>
+	</xsd:simpleType>
+
+	<xsd:simpleType name="clusterConnectionModeEnumeration">
+		<xsd:restriction base="xsd:token">
+			<xsd:enumeration value="SINGLE"/>
+			<xsd:enumeration value="MULTIPLE"/>
+		</xsd:restriction>
+	</xsd:simpleType>
+
+	<xsd:simpleType name="clusterTypeEnumeration">
+		<xsd:restriction base="xsd:token">
+			<xsd:enumeration value="STANDALONE"/>
+			<xsd:enumeration value="REPLICA_SET"/>
+			<xsd:enumeration value="SHARDED"/>
+			<xsd:enumeration value="UNKNOWN"/>
+		</xsd:restriction>
+	</xsd:simpleType>
+
+	<xsd:simpleType name="booleanType">
+		<xsd:restriction base="xsd:token">
+			<xsd:enumeration value="true"/>
+			<xsd:enumeration value="false"/>
+		</xsd:restriction>
+	</xsd:simpleType>
+
+	<xsd:complexType name="mongoClientType">
+		<xsd:annotation>
+			<xsd:documentation><![CDATA[
+Configuration options for 'com.mongodb.client.MongoClient'
+			]]></xsd:documentation>
+		</xsd:annotation>
+		<xsd:sequence minOccurs="0" maxOccurs="1">
+			<xsd:element name="client-settings" type="clientSettingsType">
+				<xsd:annotation>
+					<xsd:documentation><![CDATA[
+The Mongo driver settings
+							]]></xsd:documentation>
+					<xsd:appinfo>
+						<tool:annotation>
+							<tool:exports type="com.mongodb.MongoClientSettings"/>
+						</tool:annotation>
+					</xsd:appinfo>
+				</xsd:annotation>
+			</xsd:element>
+		</xsd:sequence>
+		<xsd:attribute name="id" type="xsd:string" use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+The name of the MongoClient definition (by default "mongoClient").]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="connection-string" type="xsd:string" use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+The mongodb connection string. E.g. 'mongodb://localhost:27017?replicaSet=rs0'
+See https://docs.mongodb.com/manual/reference/connection-string/ for full documentation.
+							]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="port" type="xsd:string" use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+The port to connect to MongoDB server.  Default is 27017
+							]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="host" type="xsd:string" use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+The host to connect to a MongoDB server.  Default is localhost
+							]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="replica-set" type="xsd:string" use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+The replica set name when connecting to a cluster.
+							]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="credential" type="xsd:string" use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+NOTE: Best way of setting up a connection is by providing the 'connection-string'.
+The comma delimited list of username:password@database entries to use for authentication. Appending ?uri.authMechanism allows to specify the authentication challenge mechanism. If the credential you're trying to pass contains a comma itself, quote it with single quotes: '…'.
+							]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+	</xsd:complexType>
+
+	<xsd:complexType name="clientSettingsType">
+		<xsd:annotation>
+			<xsd:documentation><![CDATA[
+Configuration options for 'MongoClientSettings' - @since 3.0
+			]]></xsd:documentation>
+		</xsd:annotation>
+		<xsd:attribute name="application-name" type="xsd:string" use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+The application name to use when connecting to MongoDB. Mainly used to identify an operation in server logs, query logs and other profiling features.
+				]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="uuid-representation" use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+The storage format of UUID types.
+				]]></xsd:documentation>
+			</xsd:annotation>
+			<xsd:simpleType>
+				<xsd:union memberTypes="uuidRepresentationEnumeration xsd:string"/>
+			</xsd:simpleType>
+		</xsd:attribute>
+		<xsd:attribute name="read-preference" use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+The read preference to use for quries, map-reduce, aggregation and count operations.
+				]]></xsd:documentation>
+			</xsd:annotation>
+			<xsd:simpleType>
+				<xsd:union memberTypes="readPreferenceEnumeration xsd:string"/>
+			</xsd:simpleType>
+		</xsd:attribute>
+		<xsd:attribute name="read-concern" use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+Set the global read isolation level.
+				]]></xsd:documentation>
+			</xsd:annotation>
+			<xsd:simpleType>
+				<xsd:union memberTypes="readConcernEnumeration xsd:string"/>
+			</xsd:simpleType>
+		</xsd:attribute>
+		<xsd:attribute name="write-concern">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+Set the default 'WriteConcern' that is controls the acknowledgment of write operations.
+				]]></xsd:documentation>
+			</xsd:annotation>
+			<xsd:simpleType>
+				<xsd:union memberTypes="writeConcernEnumeration xsd:string"/>
+			</xsd:simpleType>
+		</xsd:attribute>
+		<xsd:attribute name="retry-reads" use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+Sets whether reads should be retried if they fail due to a network error.
+				]]></xsd:documentation>
+			</xsd:annotation>
+			<xsd:simpleType>
+				<xsd:union memberTypes="booleanType xsd:string"/>
+			</xsd:simpleType>
+		</xsd:attribute>
+		<xsd:attribute name="retry-writes" use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+Sets whether writes should be retried if they fail due to a network error.
+				]]></xsd:documentation>
+			</xsd:annotation>
+			<xsd:simpleType>
+				<xsd:union memberTypes="booleanType xsd:string"/>
+			</xsd:simpleType>
+		</xsd:attribute>
+		<xsd:attribute name="socket-connect-timeout" type="xsd:string" use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+Sets the socket connect timeout (msec).
+				]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="socket-read-timeout" type="xsd:string" use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+Sets the socket read timeoutn (msec).
+				]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="socket-receive-buffer-size" type="xsd:string" use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+Sets the receive buffer size.
+				]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="socket-send-buffer-size" type="xsd:string" use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+Sets the send buffer size.
+				]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="server-heartbeat-frequency" type="xsd:string" use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+This is the frequency that the driver will attempt to determine the current state of each server in the cluster.
+				]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="server-min-heartbeat-frequency" type="xsd:string"
+		               use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+In the event that the driver has to frequently re-check a server's availability, it will wait at least this long since the previous check to avoid wasted effort.
+				]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="cluster-srv-host" type="xsd:string" use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+Sets the host name to use in order to look up an SRV DNS record to find the MongoDB hosts.
+NOTE: do not use along with cluster-hosts.
+				]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="cluster-hosts" type="xsd:string" use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+Sets the hosts for the cluster. Any duplicate server addresses are removed from the list.
+NOTE: do not use along with cluster-srv-host
+				]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="cluster-connection-mode" use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+Sets the cluster connection mode to either single node direct or multiple servers.
+				]]></xsd:documentation>
+			</xsd:annotation>
+			<xsd:simpleType>
+				<xsd:union memberTypes="clusterConnectionModeEnumeration xsd:string"/>
+			</xsd:simpleType>
+		</xsd:attribute>
+		<xsd:attribute name="cluster-type" use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+Sets the cluster type (eg. SHARDED, REPLICA_SET,...).
+				]]></xsd:documentation>
+			</xsd:annotation>
+			<xsd:simpleType>
+				<xsd:union memberTypes="clusterTypeEnumeration xsd:string"/>
+			</xsd:simpleType>
+		</xsd:attribute>
+		<xsd:attribute name="cluster-local-threshold" type="xsd:string" use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+Sets the local threshold when selecting a server based on fastes ping time.
+				]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="cluster-server-selection-timeout" type="xsd:string"
+		               use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+Sets the timeout to apply when selecting a server.
+Zero indicates an immediate timeout while a negative value means indefinitely wait.
+				]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="connection-pool-max-size" type="xsd:string" use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+Sets the maximum number of connections allowed.
+				]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="connection-pool-min-size" type="xsd:string" use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+Sets the minimum number of connections.
+				]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="connection-pool-max-wait-time" type="xsd:string"
+		               use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+Sets the maximum time a thread may wait for a connection to become available.
+				]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="connection-pool-max-connection-life-time" type="xsd:string"
+		               use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+The maximum time a pooled connection can live for.
+Zero indicates no limit.
+				]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="connection-pool-max-connection-idle-time" type="xsd:string"
+		               use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+Sets the maximum idle time of a pooled connection.
+Zero indicates no limit.
+				]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="connection-pool-maintenance-initial-delay" type="xsd:string"
+		               use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+Sets the period of time to wait before running the first maintenance job on the connection pool.
+				]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="connection-pool-maintenance-frequency" type="xsd:string"
+		               use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+Sets the time period between runs of the maintenance job.
+				]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="ssl-enabled" default="false">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+Set whether SSL should be enabled or not.
+				]]></xsd:documentation>
+			</xsd:annotation>
+			<xsd:simpleType>
+				<xsd:union memberTypes="booleanType xsd:string"/>
+			</xsd:simpleType>
+		</xsd:attribute>
+		<xsd:attribute name="ssl-invalid-host-name-allowed" type="xsd:string"
+		               use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+Set whether invalid host names should be allowed.
+				]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="ssl-provider" type="xsd:string" use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+Set the SSL Context instance provider.
+				]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="encryption-settings-ref" type="encryptionSettingsRef"
+		               use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+AutoEncryptionSettings for MongoDB 4.2+
+				]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="server-api-version" type="xsd:string" use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+Set the server API version for MongoDB 5.0+. Use server-api-ref if required to set additional modes like 'strict',...
+				]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+		<xsd:attribute name="server-api-ref" type="serverApiRef" use="optional">
+			<xsd:annotation>
+				<xsd:documentation><![CDATA[
+ServerAPI for MongoDB 5.0+
+				]]></xsd:documentation>
+			</xsd:annotation>
+		</xsd:attribute>
+	</xsd:complexType>
+
+	<xsd:group name="beanElementGroup">
+		<xsd:choice>
+			<xsd:element ref="beans:bean"/>
+			<xsd:element ref="beans:ref"/>
+		</xsd:choice>
+	</xsd:group>
+
+	<xsd:complexType name="customConverterType">
+		<xsd:annotation>
+			<xsd:documentation><![CDATA[
+	Element defining a custom converter.
+	]]></xsd:documentation>
+		</xsd:annotation>
+		<xsd:group ref="beanElementGroup" minOccurs="0" maxOccurs="1"/>
+		<xsd:attribute name="ref" type="xsd:string">
+			<xsd:annotation>
+				<xsd:documentation>
+					A reference to a custom converter.
+				</xsd:documentation>
+				<xsd:appinfo>
+					<tool:annotation kind="ref"/>
+				</xsd:appinfo>
+			</xsd:annotation>
+		</xsd:attribute>
+	</xsd:complexType>
+
+	<xsd:simpleType name="converterRef">
+		<xsd:annotation>
+			<xsd:appinfo>
+				<tool:annotation kind="ref">
+					<tool:assignable-to
+							type="org.springframework.data.mongodb.core.convert.MongoConverter"/>
+				</tool:annotation>
+			</xsd:appinfo>
+		</xsd:annotation>
+		<xsd:union memberTypes="xsd:string"/>
+	</xsd:simpleType>
+
+	<xsd:element name="template">
+		<xsd:annotation>
+			<xsd:documentation><![CDATA[
+Defines a MongoTemplate.
+			]]></xsd:documentation>
+		</xsd:annotation>
+		<xsd:complexType>
+			<xsd:attribute name="id" type="xsd:string" use="optional">
+				<xsd:annotation>
+					<xsd:documentation><![CDATA[
+The name of the MongoTemplate definition (by default "mongoTemplate").]]></xsd:documentation>
+				</xsd:annotation>
+			</xsd:attribute>
+			<xsd:attribute name="converter-ref" type="converterRef" use="optional">
+				<xsd:annotation>
+					<xsd:documentation><![CDATA[
+The reference to a MappingMongoConverter instance.
+					]]>
+					</xsd:documentation>
+					<xsd:appinfo>
+						<tool:annotation kind="ref">
+							<tool:assignable-to
+									type="org.springframework.data.mongodb.core.convert.MongoConverter"/>
+						</tool:annotation>
+					</xsd:appinfo>
+				</xsd:annotation>
+			</xsd:attribute>
+			<xsd:attribute name="db-factory-ref" type="xsd:string"
+			               use="optional">
+				<xsd:annotation>
+					<xsd:documentation>
+						The reference to a MongoDatabaseFactory.
+					</xsd:documentation>
+					<xsd:appinfo>
+						<tool:annotation kind="ref">
+							<tool:assignable-to
+									type="org.springframework.data.mongodb.MongoDatabaseFactory"/>
+						</tool:annotation>
+					</xsd:appinfo>
+				</xsd:annotation>
+			</xsd:attribute>
+			<xsd:attribute name="write-concern">
+				<xsd:annotation>
+					<xsd:documentation>
+						The WriteConcern that will be the default value used when asking
+						the MongoDatabaseFactory for a DB object
+					</xsd:documentation>
+				</xsd:annotation>
+				<xsd:simpleType>
+					<xsd:union memberTypes="writeConcernEnumeration xsd:string"/>
+				</xsd:simpleType>
+			</xsd:attribute>
+		</xsd:complexType>
+	</xsd:element>
+
+	<xsd:element name="gridFsTemplate">
+		<xsd:annotation>
+			<xsd:documentation><![CDATA[
+Defines a GridFsTemplate.
+			]]></xsd:documentation>
+		</xsd:annotation>
+		<xsd:complexType>
+			<xsd:attribute name="id" type="xsd:string" use="optional">
+				<xsd:annotation>
+					<xsd:documentation><![CDATA[
+The name of the GridFsTemplate definition (by default "gridFsTemplate").]]></xsd:documentation>
+				</xsd:annotation>
+			</xsd:attribute>
+			<xsd:attribute name="converter-ref" type="converterRef" use="optional">
+				<xsd:annotation>
+					<xsd:documentation><![CDATA[
+The reference to a MappingMongoConverter instance.
+					]]>
+					</xsd:documentation>
+					<xsd:appinfo>
+						<tool:annotation kind="ref">
+							<tool:assignable-to
+									type="org.springframework.data.mongodb.core.convert.MongoConverter"/>
+						</tool:annotation>
+					</xsd:appinfo>
+				</xsd:annotation>
+			</xsd:attribute>
+			<xsd:attribute name="db-factory-ref" type="xsd:string" use="optional">
+				<xsd:annotation>
+					<xsd:documentation>
+						The reference to a MongoDatabaseFactory.
+					</xsd:documentation>
+					<xsd:appinfo>
+						<tool:annotation kind="ref">
+							<tool:assignable-to
+									type="org.springframework.data.mongodb.MongoDatabaseFactory"/>
+						</tool:annotation>
+					</xsd:appinfo>
+				</xsd:annotation>
+			</xsd:attribute>
+			<xsd:attribute name="bucket" type="xsd:string" use="optional">
+				<xsd:annotation>
+					<xsd:documentation><![CDATA[
+The GridFs bucket string.]]></xsd:documentation>
+				</xsd:annotation>
+			</xsd:attribute>
+		</xsd:complexType>
+	</xsd:element>
+</xsd:schema>
diff --git a/spring-data-mongodb/src/test/java/example/aot/User.java b/spring-data-mongodb/src/test/java/example/aot/User.java
new file mode 100644
index 0000000000..06022c0a55
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/example/aot/User.java
@@ -0,0 +1,94 @@
+/*
+ * 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.aot;
+
+import java.time.Instant;
+
+import org.springframework.data.mongodb.core.mapping.Field;
+
+/**
+ * @author Christoph Strobl
+ */
+public class User {
+
+	String id;
+
+	String username;
+
+	@Field("first_name") String firstname;
+
+	@Field("last_name") String lastname;
+
+	Instant registrationDate;
+	Instant lastSeen;
+	Long visits;
+
+	public String getId() {
+		return id;
+	}
+
+	public void setId(String id) {
+		this.id = id;
+	}
+
+	public String getUsername() {
+		return username;
+	}
+
+	public void setUsername(String username) {
+		this.username = username;
+	}
+
+	public String getFirstname() {
+		return firstname;
+	}
+
+	public void setFirstname(String firstname) {
+		this.firstname = firstname;
+	}
+
+	public String getLastname() {
+		return lastname;
+	}
+
+	public void setLastname(String lastname) {
+		this.lastname = lastname;
+	}
+
+	public Instant getRegistrationDate() {
+		return registrationDate;
+	}
+
+	public void setRegistrationDate(Instant registrationDate) {
+		this.registrationDate = registrationDate;
+	}
+
+	public Instant getLastSeen() {
+		return lastSeen;
+	}
+
+	public void setLastSeen(Instant lastSeen) {
+		this.lastSeen = lastSeen;
+	}
+
+	public Long getVisits() {
+		return visits;
+	}
+
+	public void setVisits(Long visits) {
+		this.visits = visits;
+	}
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/monitor/Resumeable.java b/spring-data-mongodb/src/test/java/example/aot/UserProjection.java
similarity index 73%
rename from spring-data-mongodb/src/test/java/org/springframework/data/mongodb/monitor/Resumeable.java
rename to spring-data-mongodb/src/test/java/example/aot/UserProjection.java
index 1fdbb1f188..e59598d3a9 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/monitor/Resumeable.java
+++ b/spring-data-mongodb/src/test/java/example/aot/UserProjection.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-2025 the original author or authors.
+ * 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.
@@ -13,15 +13,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.springframework.data.mongodb.monitor;
+package example.aot;
 
-import java.util.function.Supplier;
+import java.time.Instant;
 
 /**
  * @author Christoph Strobl
- * @since 2018/01
  */
-interface Resumeable<T> {
+public interface UserProjection {
 
-	void resumeAt(Supplier<T> token);
+	String getUsername();
+
+	Instant getLastSeen();
 }
diff --git a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java
new file mode 100644
index 0000000000..5eb9fed686
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java
@@ -0,0 +1,290 @@
+/*
+ * 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.aot;
+
+import java.time.Instant;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.domain.Limit;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.ScrollPosition;
+import org.springframework.data.domain.Slice;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Window;
+import org.springframework.data.mongodb.core.aggregation.AggregationResults;
+import org.springframework.data.mongodb.repository.Aggregation;
+import org.springframework.data.mongodb.repository.Hint;
+import org.springframework.data.mongodb.repository.Query;
+import org.springframework.data.mongodb.repository.ReadPreference;
+import org.springframework.data.mongodb.repository.Update;
+import org.springframework.data.repository.CrudRepository;
+
+/**
+ * @author Christoph Strobl
+ */
+public interface UserRepository extends CrudRepository<User, String> {
+
+	/* Derived Queries */
+
+	List<User> findUserNoArgumentsBy();
+
+	User findOneByUsername(String username);
+
+	Optional<User> findOptionalOneByUsername(String username);
+
+	Long countUsersByLastname(String lastname);
+
+	int countUsersAsIntByLastname(String lastname);
+
+	Boolean existsUserByLastname(String lastname);
+
+	List<User> findByLastnameStartingWith(String lastname);
+
+	List<User> findByLastnameEndsWith(String postfix);
+
+	List<User> findByFirstnameLike(String firstname);
+
+	List<User> findByFirstnameNotLike(String firstname);
+
+	List<User> findByUsernameIn(Collection<String> usernames);
+
+	List<User> findByUsernameNotIn(Collection<String> usernames);
+
+	List<User> findByFirstnameAndLastname(String firstname, String lastname);
+
+	List<User> findByFirstnameOrLastname(String firstname, String lastname);
+
+	List<User> findByVisitsBetween(long from, long to);
+
+	List<User> findByLastSeenGreaterThan(Instant time);
+
+	List<User> findByVisitsExists(boolean exists);
+
+	List<User> findByLastnameNot(String lastname);
+
+	List<User> findTop2ByLastnameStartingWith(String lastname);
+
+	List<User> findByLastnameStartingWithOrderByUsername(String lastname);
+
+	List<User> findByLastnameStartingWith(String lastname, Limit limit);
+
+	List<User> findByLastnameStartingWith(String lastname, Sort sort);
+
+	List<User> findByLastnameStartingWith(String lastname, Sort sort, Limit limit);
+
+	List<User> findByLastnameStartingWith(String lastname, Pageable page);
+
+	Page<User> findPageOfUsersByLastnameStartingWith(String lastname, Pageable page);
+
+	Slice<User> findSliceOfUserByLastnameStartingWith(String lastname, Pageable page);
+
+	Stream<User> streamByLastnameStartingWith(String lastname, Sort sort, Limit limit);
+
+	Window<User> findTop2WindowByLastnameStartingWithOrderByUsername(String lastname, ScrollPosition scrollPosition);
+
+	// TODO: GeoQueries
+	// TODO: TextSearch
+
+	/* Annotated Queries */
+
+	@Query("{ 'username' : ?0 }")
+	User findAnnotatedQueryByUsername(String username);
+
+	@Query(value = "{ 'lastname' : { '$regex' : '^?0' } }", count = true)
+	Long countAnnotatedQueryByLastname(String lastname);
+
+	@Query("{ 'lastname' : { '$regex' : '^?0' } }")
+	List<User> findAnnotatedQueryByLastname(String lastname);
+
+	@Query("""
+			{
+			    'lastname' : {
+			        '$regex' : '^?0'
+			    }
+			}""")
+	List<User> findAnnotatedMultilineQueryByLastname(String username);
+
+	@Query("{ 'lastname' : { '$regex' : '^?0' } }")
+	List<User> findAnnotatedQueryByLastname(String lastname, Limit limit);
+
+	@Query("{ 'lastname' : { '$regex' : '^?0' } }")
+	List<User> findAnnotatedQueryByLastname(String lastname, Sort sort);
+
+	@Query("{ 'lastname' : { '$regex' : '^?0' } }")
+	List<User> findAnnotatedQueryByLastname(String lastname, Limit limit, Sort sort);
+
+	@Query("{ 'lastname' : { '$regex' : '^?0' } }")
+	List<User> findAnnotatedQueryByLastname(String lastname, Pageable pageable);
+
+	@Query("{ 'lastname' : { '$regex' : '^?0' } }")
+	Page<User> findAnnotatedQueryPageOfUsersByLastname(String lastname, Pageable pageable);
+
+	@Query("{ 'lastname' : { '$regex' : '^?0' } }")
+	Slice<User> findAnnotatedQuerySliceOfUsersByLastname(String lastname, Pageable pageable);
+
+	/* deletes */
+
+	User deleteByUsername(String username);
+
+	@Query(value = "{ 'username' : ?0 }", delete = true)
+	User deleteAnnotatedQueryByUsername(String username);
+
+	Long deleteByLastnameStartingWith(String lastname);
+
+	@Query(value = "{ 'lastname' : { '$regex' : '^?0' } }", delete = true)
+	Long deleteAnnotatedQueryByLastnameStartingWith(String lastname);
+
+	List<User> deleteUsersByLastnameStartingWith(String lastname);
+
+	@Query(value = "{ 'lastname' : { '$regex' : '^?0' } }", delete = true)
+	List<User> deleteUsersAnnotatedQueryByLastnameStartingWith(String lastname);
+
+	/* Updates */
+
+	@Update("{ '$inc' : { 'visits' : ?1 } }")
+	int findUserAndIncrementVisitsByLastname(String lastname, int increment);
+
+	@Query("{ 'lastname' : ?0 }")
+	@Update("{ '$inc' : { 'visits' : ?1 } }")
+	int updateAllByLastname(String lastname, int increment);
+
+	@Update(pipeline = { "{ '$set' : { 'visits' : { '$ifNull' : [ {'$add' : [ '$visits', ?1 ] }, ?1 ] } } }" })
+	void findAndIncrementVisitsViaPipelineByLastname(String lastname, int increment);
+
+	/* Derived With Annotated Options */
+
+	@Query(sort = "{ 'username' : 1 }")
+	List<User> findWithAnnotatedSortByLastnameStartingWith(String lastname);
+
+	@Query(fields = "{ 'username' : 1 }")
+	List<User> findWithAnnotatedFieldsProjectionByLastnameStartingWith(String lastname);
+
+	@ReadPreference("no-such-read-preference")
+	User findWithReadPreferenceByUsername(String username);
+
+	/* Projecting Queries */
+
+	List<UserProjection> findUserProjectionByLastnameStartingWith(String lastname);
+
+	Page<UserProjection> findUserProjectionByLastnameStartingWith(String lastname, Pageable page);
+
+	<T> Page<T> findUserProjectionByLastnameStartingWith(String lastname, Pageable page, Class<T> projectionType);
+
+	/* Aggregations */
+
+	@Aggregation(pipeline = { //
+			"{ '$match' : { 'last_name' : { '$ne' : null } } }", //
+			"{ '$project': { '_id' : '$last_name' } }" })
+	List<String> findAllLastnames();
+
+	@Aggregation(pipeline = { //
+			"{ '$match' : { 'last_name' : { '$ne' : null } } }", //
+			"{ '$group': { '_id' : '$last_name', names : { $addToSet : '$?0' } } }" })
+	List<UserAggregate> groupByLastnameAnd(String property);
+
+	@Aggregation(pipeline = { //
+			"{ '$match' : { 'last_name' : { '$ne' : null } } }", //
+			"{ '$group': { '_id' : '$last_name', names : { $addToSet : '$?0' } } }" })
+	List<UserAggregate> groupByLastnameAnd(String property, Pageable pageable);
+
+	@Aggregation(pipeline = { //
+			"{ '$match' : { 'last_name' : { '$ne' : null } } }", //
+			"{ '$group': { '_id' : '$last_name', names : { $addToSet : '$?0' } } }" })
+	Slice<UserAggregate> groupByLastnameAndReturnPage(String property, Pageable pageable);
+
+	@Aggregation(pipeline = { //
+			"{ '$match' : { 'last_name' : { '$ne' : null } } }", //
+			"{ '$group': { '_id' : '$last_name', names : { $addToSet : '$?0' } } }" })
+	AggregationResults<UserAggregate> groupByLastnameAndAsAggregationResults(String property);
+
+	@Aggregation(pipeline = { //
+			"{ '$match' : { 'last_name' : { '$ne' : null } } }", //
+			"{ '$group': { '_id' : '$last_name', names : { $addToSet : '$?0' } } }" })
+	Stream<UserAggregate> streamGroupByLastnameAndAsAggregationResults(String property);
+
+	@Aggregation(pipeline = { //
+			"{ '$match' : { 'posts' : { '$ne' : null } } }", //
+			"{ '$project': { 'nrPosts' : {'$size': '$posts' } } }", //
+			"{ '$group' : { '_id' : null, 'total' : { $sum: '$nrPosts' } } }" })
+	int sumPosts();
+
+	@Hint("ln-idx")
+	@Aggregation(pipeline = { //
+			"{ '$match' : { 'last_name' : { '$ne' : null } } }", //
+			"{ '$project': { '_id' : '$last_name' } }" })
+	List<String> findAllLastnamesUsingIndex();
+
+	@ReadPreference("no-such-read-preference")
+	@Aggregation(pipeline = { //
+			"{ '$match' : { 'last_name' : { '$ne' : null } } }", //
+			"{ '$project': { '_id' : '$last_name' } }" })
+	List<String> findAllLastnamesWithReadPreference();
+
+	@Aggregation(pipeline = { //
+			"{ '$match' : { 'last_name' : { '$ne' : null } } }", //
+			"{ '$project': { '_id' : '$last_name' } }" }, collation = "no_collation")
+	List<String> findAllLastnamesWithCollation();
+
+	class UserAggregate {
+
+		@Id //
+		private final String lastname;
+		private final Set<String> names;
+
+		public UserAggregate(String lastname, Collection<String> names) {
+			this.lastname = lastname;
+			this.names = new HashSet<>(names);
+		}
+
+		public String getLastname() {
+			return this.lastname;
+		}
+
+		public Set<String> getNames() {
+			return this.names;
+		}
+
+		@Override
+		public String toString() {
+			return "UserAggregate{" + "lastname='" + lastname + '\'' + ", names=" + names + '}';
+		}
+
+		@Override
+		public boolean equals(Object o) {
+			if (o == this) {
+				return true;
+			}
+			if (o == null || getClass() != o.getClass()) {
+				return false;
+			}
+			UserAggregate that = (UserAggregate) o;
+			return Objects.equals(lastname, that.lastname) && names.equals(that.names);
+		}
+
+		@Override
+		public int hashCode() {
+			return Objects.hash(lastname, names);
+		}
+	}
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/CapturingTransactionOptionsResolver.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/CapturingTransactionOptionsResolver.java
index 0448ad936c..c05122873c 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/CapturingTransactionOptionsResolver.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/CapturingTransactionOptionsResolver.java
@@ -21,7 +21,7 @@
 
 import org.assertj.core.api.Assertions;
 import org.assertj.core.api.ListAssert;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.CollectionUtils;
 
 /**
@@ -36,9 +36,8 @@ public CapturingTransactionOptionsResolver(MongoTransactionOptionsResolver deleg
 		this.delegateResolver = delegateResolver;
 	}
 
-	@Nullable
 	@Override
-	public String getLabelPrefix() {
+	public @Nullable String getLabelPrefix() {
 		return delegateResolver.getLabelPrefix();
 	}
 
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/MongoTransactionOptionsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/MongoTransactionOptionsUnitTests.java
index 44692348a0..d89edc6206 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/MongoTransactionOptionsUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/MongoTransactionOptionsUnitTests.java
@@ -20,8 +20,8 @@
 import java.time.Duration;
 import java.util.concurrent.TimeUnit;
 
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.Test;
-import org.springframework.lang.Nullable;
 
 import com.mongodb.ReadConcern;
 import com.mongodb.ReadPreference;
@@ -90,27 +90,23 @@ void testEquals() {
 		assertThat(MongoTransactionOptions.NONE) //
 				.isSameAs(MongoTransactionOptions.NONE) //
 				.isNotEqualTo(new MongoTransactionOptions() {
-					@Nullable
 					@Override
-					public Duration getMaxCommitTime() {
+					public @Nullable Duration getMaxCommitTime() {
 						return null;
 					}
 
-					@Nullable
 					@Override
-					public ReadConcern getReadConcern() {
+					public @Nullable ReadConcern getReadConcern() {
 						return null;
 					}
 
-					@Nullable
 					@Override
-					public ReadPreference getReadPreference() {
+					public @Nullable ReadPreference getReadPreference() {
 						return null;
 					}
 
-					@Nullable
 					@Override
-					public WriteConcern getWriteConcern() {
+					public @Nullable WriteConcern getWriteConcern() {
 						return null;
 					}
 				});
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/DemoRepo.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/DemoRepo.java
new file mode 100644
index 0000000000..41759e68c7
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/DemoRepo.java
@@ -0,0 +1,46 @@
+/*
+ * 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.mongodb.aot.generated;
+
+import java.util.List;
+
+import example.aot.User;
+import org.springframework.data.mongodb.BindableMongoExpression;
+import org.springframework.data.mongodb.core.MongoOperations;
+import org.springframework.data.mongodb.core.query.BasicQuery;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.mongodb.repository.query.StringBasedMongoQuery;
+
+/**
+ * @author Christoph Strobl
+ * @since 2025/01
+ */
+public class DemoRepo {
+
+
+    MongoOperations operations;
+
+    List<User> method1(String username) {
+
+        BindableMongoExpression filter = new BindableMongoExpression("{ 'username', ?0 }", operations.getConverter(), new Object[]{username});
+        Query query = new BasicQuery(filter.toDocument());
+
+        return operations.query(User.class)
+            .as(User.class)
+            .matching(query)
+            .all();
+    }
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CollectionOptionsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CollectionOptionsUnitTests.java
index f2691275c3..9de0863cd2 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CollectionOptionsUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CollectionOptionsUnitTests.java
@@ -15,12 +15,24 @@
  */
 package org.springframework.data.mongodb.core;
 
-import static org.assertj.core.api.Assertions.*;
-import static org.springframework.data.mongodb.core.CollectionOptions.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.data.mongodb.core.CollectionOptions.EncryptedFieldsOptions;
+import static org.springframework.data.mongodb.core.CollectionOptions.TimeSeriesOptions;
+import static org.springframework.data.mongodb.core.CollectionOptions.emitChangedRevisions;
+import static org.springframework.data.mongodb.core.CollectionOptions.empty;
+import static org.springframework.data.mongodb.core.CollectionOptions.encryptedCollection;
+import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.int32;
+import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.queryable;
 
+import java.util.List;
+
+import org.bson.BsonNull;
 import org.bson.Document;
 import org.junit.jupiter.api.Test;
 import org.springframework.data.mongodb.core.query.Collation;
+import org.springframework.data.mongodb.core.schema.JsonSchemaProperty;
+import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
+import org.springframework.data.mongodb.core.schema.QueryCharacteristics;
 import org.springframework.data.mongodb.core.validation.Validator;
 
 /**
@@ -76,4 +88,93 @@ void validatorEquals() {
 				.isNotEqualTo(empty().validator(Validator.document(new Document("three", "four"))))
 				.isNotEqualTo(empty().validator(Validator.document(new Document("one", "two"))).moderateValidation());
 	}
+
+	@Test // GH-4185
+	@SuppressWarnings("unchecked")
+	void queryableEncryptionOptionsFromSchemaRenderCorrectly() {
+
+		MongoJsonSchema schema = MongoJsonSchema.builder()
+				.property(JsonSchemaProperty.object("spring")
+						.properties(queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int32("data")), List.of())))
+				.property(queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int64("mongodb")), List.of())).build();
+
+		EncryptedFieldsOptions encryptionOptions = EncryptedFieldsOptions.fromSchema(schema);
+
+		assertThat(encryptionOptions.toDocument().get("fields", List.class)).hasSize(2)
+				.contains(new Document("path", "mongodb").append("bsonType", "long").append("queries", List.of())
+						.append("keyId", BsonNull.VALUE))
+				.contains(new Document("path", "spring.data").append("bsonType", "int").append("queries", List.of())
+						.append("keyId", BsonNull.VALUE));
+	}
+
+	@Test // GH-4185
+	@SuppressWarnings("unchecked")
+	void queryableEncryptionPropertiesOverrideByPath() {
+
+		CollectionOptions collectionOptions = encryptedCollection(options -> options //
+				.queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int32("spring")))
+				.queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int64("data")))
+
+				// override first with data type long
+				.queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int64("spring"))));
+
+		assertThat(collectionOptions.getEncryptedFieldsOptions()).map(EncryptedFieldsOptions::toDocument)
+				.hasValueSatisfying(it -> {
+					assertThat(it.get("fields", List.class)).hasSize(2).contains(new Document("path", "spring")
+							.append("bsonType", "long").append("queries", List.of()).append("keyId", BsonNull.VALUE));
+				});
+	}
+
+	@Test // GH-4185
+	@SuppressWarnings("unchecked")
+	void queryableEncryptionPropertiesOverridesPathFromSchema() {
+
+		EncryptedFieldsOptions encryptionOptions = EncryptedFieldsOptions.fromSchema(MongoJsonSchema.builder()
+				.property(queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int32("spring")), List.of()))
+				.property(queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int64("data")), List.of())).build());
+
+		// override spring from schema with data type long
+		CollectionOptions collectionOptions = CollectionOptions.encryptedCollection(
+				encryptionOptions.queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int64("spring"))));
+
+		assertThat(collectionOptions.getEncryptedFieldsOptions()).map(EncryptedFieldsOptions::toDocument)
+				.hasValueSatisfying(it -> {
+					assertThat(it.get("fields", List.class)).hasSize(2).contains(new Document("path", "spring")
+							.append("bsonType", "long").append("queries", List.of()).append("keyId", BsonNull.VALUE));
+				});
+	}
+
+	@Test // GH-4185
+	void encryptionOptionsAreImmutable() {
+
+		EncryptedFieldsOptions source = EncryptedFieldsOptions
+				.fromProperties(List.of(queryable(int32("spring.data"), List.of(QueryCharacteristics.range().min(1)))));
+
+		assertThat(source.queryable(queryable(int32("mongodb"), List.of(QueryCharacteristics.range().min(1)))))
+				.isNotSameAs(source).satisfies(it -> {
+					assertThat(it.toDocument().get("fields", List.class)).hasSize(2);
+				});
+
+		assertThat(source.toDocument().get("fields", List.class)).hasSize(1);
+	}
+
+	@Test // GH-4185
+	@SuppressWarnings("unchecked")
+	void queryableEncryptionPropertiesOverridesNestedPathFromSchema() {
+
+		EncryptedFieldsOptions encryptionOptions = EncryptedFieldsOptions.fromSchema(MongoJsonSchema.builder()
+				.property(JsonSchemaProperty.object("spring")
+						.properties(queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int32("data")), List.of())))
+				.property(queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int64("mongodb")), List.of())).build());
+
+		// override spring from schema with data type long
+		CollectionOptions collectionOptions = CollectionOptions.encryptedCollection(
+				encryptionOptions.queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int64("spring.data"))));
+
+		assertThat(collectionOptions.getEncryptedFieldsOptions()).map(EncryptedFieldsOptions::toDocument)
+				.hasValueSatisfying(it -> {
+					assertThat(it.get("fields", List.class)).hasSize(2).contains(new Document("path", "spring.data")
+							.append("bsonType", "long").append("queries", List.of()).append("keyId", BsonNull.VALUE));
+				});
+	}
 }
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsIntegrationTests.java
index af4fac84b1..78a6e6b496 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsIntegrationTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsIntegrationTests.java
@@ -15,9 +15,9 @@
  */
 package org.springframework.data.mongodb.core;
 
-import static org.assertj.core.api.Assertions.*;
-import static org.springframework.data.mongodb.core.index.PartialIndexFilter.*;
-import static org.springframework.data.mongodb.core.query.Criteria.*;
+import static org.springframework.data.mongodb.core.index.PartialIndexFilter.of;
+import static org.springframework.data.mongodb.core.query.Criteria.where;
+import static org.springframework.data.mongodb.test.util.Assertions.assertThat;
 
 import org.bson.BsonDocument;
 import org.bson.Document;
@@ -79,7 +79,7 @@ public void shouldApplyPartialFilterCorrectly() {
 		IndexDefinition id = new Index().named("partial-with-criteria").on("k3y", Direction.ASC)
 				.partial(of(where("q-t-y").gte(10)));
 
-		indexOps.ensureIndex(id);
+		indexOps.createIndex(id);
 
 		IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "partial-with-criteria");
 		assertThat(Document.parse(info.getPartialFilterExpression()))
@@ -92,7 +92,7 @@ public void shouldApplyPartialFilterWithMappedPropertyCorrectly() {
 		IndexDefinition id = new Index().named("partial-with-mapped-criteria").on("k3y", Direction.ASC)
 				.partial(of(where("quantity").gte(10)));
 
-		template.indexOps(DefaultIndexOperationsIntegrationTestsSample.class).ensureIndex(id);
+		template.indexOps(DefaultIndexOperationsIntegrationTestsSample.class).createIndex(id);
 
 		IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "partial-with-mapped-criteria");
 		assertThat(Document.parse(info.getPartialFilterExpression()))
@@ -105,7 +105,7 @@ public void shouldApplyPartialDBOFilterCorrectly() {
 		IndexDefinition id = new Index().named("partial-with-dbo").on("k3y", Direction.ASC)
 				.partial(of(new org.bson.Document("qty", new org.bson.Document("$gte", 10))));
 
-		indexOps.ensureIndex(id);
+		indexOps.createIndex(id);
 
 		IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "partial-with-dbo");
 		assertThat(Document.parse(info.getPartialFilterExpression()))
@@ -120,7 +120,7 @@ public void shouldFavorExplicitMappingHintViaClass() {
 
 		indexOps = new DefaultIndexOperations(template, COLLECTION_NAME, MappingToSameCollection.class);
 
-		indexOps.ensureIndex(id);
+		indexOps.createIndex(id);
 
 		IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "partial-with-inheritance");
 		assertThat(Document.parse(info.getPartialFilterExpression()))
@@ -150,7 +150,7 @@ public void shouldCreateIndexWithCollationCorrectly() {
 
 		new DefaultIndexOperations(template, COLLECTION_NAME, MappingToSameCollection.class);
 
-		indexOps.ensureIndex(id);
+		indexOps.createIndex(id);
 
 		Document expected = new Document("locale", "de_AT") //
 				.append("caseLevel", false) //
@@ -179,7 +179,7 @@ void indexShouldNotBeHiddenByDefault() {
 		IndexDefinition index = new Index().named("my-index").on("a", Direction.ASC);
 
 		indexOps = new DefaultIndexOperations(template, COLLECTION_NAME, MappingToSameCollection.class);
-		indexOps.ensureIndex(index);
+		indexOps.createIndex(index);
 
 		IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "my-index");
 		assertThat(info.isHidden()).isFalse();
@@ -191,7 +191,7 @@ void shouldCreateHiddenIndex() {
 		IndexDefinition index = new Index().named("my-hidden-index").on("a", Direction.ASC).hidden();
 
 		indexOps = new DefaultIndexOperations(template, COLLECTION_NAME, MappingToSameCollection.class);
-		indexOps.ensureIndex(index);
+		indexOps.createIndex(index);
 
 		IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "my-hidden-index");
 		assertThat(info.isHidden()).isTrue();
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupportUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupportUnitTests.java
index 05f0695839..80373562c8 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupportUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupportUnitTests.java
@@ -33,6 +33,7 @@
  * Unit tests for {@link ExecutableAggregationOperationSupport}.
  *
  * @author Christoph Strobl
+ * @author Mark Paluch
  */
 @ExtendWith(MockitoExtension.class)
 public class ExecutableAggregationOperationSupportUnitTests {
@@ -72,7 +73,8 @@ void aggregateWithUntypedAggregationAndExplicitCollection() {
 		opSupport.aggregateAndReturn(Person.class).inCollection("star-wars").by(newAggregation(project("foo"))).all();
 
 		ArgumentCaptor<Class> captor = ArgumentCaptor.forClass(Class.class);
-		verify(template).aggregate(any(Aggregation.class), eq("star-wars"), captor.capture());
+		verify(template).doAggregate(any(Aggregation.class), eq("star-wars"), captor.capture(),
+				eq(QueryResultConverter.entity()));
 		assertThat(captor.getValue()).isEqualTo(Person.class);
 	}
 
@@ -86,7 +88,8 @@ void aggregateWithUntypedAggregation() {
 		ArgumentCaptor<Class> captor = ArgumentCaptor.forClass(Class.class);
 
 		verify(template).getCollectionName(captor.capture());
-		verify(template).aggregate(any(Aggregation.class), eq("person"), captor.capture());
+		verify(template).doAggregate(any(Aggregation.class), eq("person"), captor.capture(),
+				eq(QueryResultConverter.entity()));
 
 		assertThat(captor.getAllValues()).containsExactly(Person.class, Person.class);
 	}
@@ -101,7 +104,8 @@ void aggregateWithTypeAggregation() {
 		ArgumentCaptor<Class> captor = ArgumentCaptor.forClass(Class.class);
 
 		verify(template).getCollectionName(captor.capture());
-		verify(template).aggregate(any(Aggregation.class), eq("person"), captor.capture());
+		verify(template).doAggregate(any(Aggregation.class), eq("person"), captor.capture(),
+				eq(QueryResultConverter.entity()));
 
 		assertThat(captor.getAllValues()).containsExactly(Person.class, Jedi.class);
 	}
@@ -112,7 +116,8 @@ void aggregateStreamWithUntypedAggregationAndExplicitCollection() {
 		opSupport.aggregateAndReturn(Person.class).inCollection("star-wars").by(newAggregation(project("foo"))).stream();
 
 		ArgumentCaptor<Class> captor = ArgumentCaptor.forClass(Class.class);
-		verify(template).aggregateStream(any(Aggregation.class), eq("star-wars"), captor.capture());
+		verify(template).doAggregateStream(any(Aggregation.class), eq("star-wars"), captor.capture(),
+				eq(QueryResultConverter.entity()), any());
 		assertThat(captor.getValue()).isEqualTo(Person.class);
 	}
 
@@ -126,7 +131,8 @@ void aggregateStreamWithUntypedAggregation() {
 		ArgumentCaptor<Class> captor = ArgumentCaptor.forClass(Class.class);
 
 		verify(template).getCollectionName(captor.capture());
-		verify(template).aggregateStream(any(Aggregation.class), eq("person"), captor.capture());
+		verify(template).doAggregateStream(any(Aggregation.class), eq("person"), captor.capture(),
+				eq(QueryResultConverter.entity()), any());
 
 		assertThat(captor.getAllValues()).containsExactly(Person.class, Person.class);
 	}
@@ -141,7 +147,8 @@ void aggregateStreamWithTypeAggregation() {
 		ArgumentCaptor<Class> captor = ArgumentCaptor.forClass(Class.class);
 
 		verify(template).getCollectionName(captor.capture());
-		verify(template).aggregateStream(any(Aggregation.class), eq("person"), captor.capture());
+		verify(template).doAggregateStream(any(Aggregation.class), eq("person"), captor.capture(),
+				eq(QueryResultConverter.entity()), any());
 
 		assertThat(captor.getAllValues()).containsExactly(Person.class, Jedi.class);
 	}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java
index eac248e69a..835367990a 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java
@@ -21,7 +21,9 @@
 import static org.springframework.data.mongodb.test.util.DirtiesStateExtension.*;
 
 import java.util.Date;
+import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.stream.Stream;
 
 import org.bson.BsonString;
@@ -170,6 +172,16 @@ void findAllByWithProjection() {
 				.hasOnlyElementsOfType(Jedi.class).hasSize(1);
 	}
 
+	@Test // GH-4949
+	void findAllByWithConverter() {
+
+		List<Optional<Jedi>> result = template.query(Person.class).as(Jedi.class)
+				.matching(query(where("firstname").is("luke"))).map((document, reader) -> Optional.of(reader.get())).all();
+
+		assertThat(result).hasOnlyElementsOfType(Optional.class).hasSize(1);
+		assertThat(result).extracting(Optional::get).hasOnlyElementsOfType(Jedi.class).hasSize(1);
+	}
+
 	@Test // DATAMONGO-1563
 	void findBy() {
 		assertThat(template.query(Person.class).matching(query(where("firstname").is("luke"))).one()).contains(luke);
@@ -260,6 +272,15 @@ void streamAllWithProjection() {
 		}
 	}
 
+	@Test // GH-4949
+	void streamAllWithConverter() {
+
+		try (Stream<Optional<Jedi>> stream = template.query(Person.class).as(Jedi.class)
+				.map((document, reader) -> Optional.of(reader.get())).stream()) {
+			assertThat(stream).extracting(Optional::get).hasOnlyElementsOfType(Jedi.class).hasSize(2);
+		}
+	}
+
 	@Test // DATAMONGO-1733
 	void streamAllReturningResultsAsClosedInterfaceProjection() {
 
@@ -315,6 +336,20 @@ void findAllNearByWithCollectionAndProjection() {
 		assertThat(results.getContent().get(0).getContent().getId()).isEqualTo("alderan");
 	}
 
+	@Test // GH-4949
+	void findAllNearByWithConverter() {
+
+		GeoResults<Optional<Human>> results = template.query(Object.class).inCollection(STAR_WARS_PLANETS).as(Human.class)
+				.near(NearQuery.near(-73.9667, 40.78).spherical(true)).map((document, reader) -> Optional.of(reader.get()))
+				.all();
+
+		assertThat(results.getContent()).hasSize(2);
+		assertThat(results.getContent().get(0).getDistance()).isNotNull();
+		assertThat(results.getContent().get(0).getContent()).isInstanceOf(Optional.class);
+		assertThat(results.getContent().get(0).getContent().get()).isInstanceOf(Human.class);
+		assertThat(results.getContent().get(0).getContent().get().getId()).isEqualTo("alderan");
+	}
+
 	@Test // DATAMONGO-1733
 	void findAllNearByReturningGeoResultContentAsClosedInterfaceProjection() {
 
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupportTests.java
index 621e2a0764..fe19672068 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupportTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupportTests.java
@@ -21,6 +21,7 @@
 
 import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -108,6 +109,14 @@ void removeAndReturnAllMatching() {
 		assertThat(result).containsExactly(han);
 	}
 
+	@Test // GH-4949
+	void removeAndReturnAllMatchingWithResultConverter() {
+
+		List<Optional<Person>> result = template.remove(Person.class).matching(query(where("firstname").is("han"))).map((raw, converted) -> Optional.of(converted.get())).findAndRemove();
+
+		assertThat(result).containsExactly(Optional.of(han));
+	}
+
 	@org.springframework.data.mongodb.core.mapping.Document(collection = STAR_WARS)
 	static class Person {
 
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java
index e7f50dab53..46732b1a29 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java
@@ -185,6 +185,17 @@ void findAndModifyWithOptions() {
 		assertThat(result.get()).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", "Han");
 	}
 
+	@Test // GH-4949
+	void findAndModifyWithResultConverter() {
+
+		Optional<Person> result = template.update(Person.class).matching(queryHan())
+			.apply(new Update().set("firstname", "Han")).withOptions(FindAndModifyOptions.options().returnNew(true))
+			.map((raw, converted) -> Optional.of(converted.get()))
+			.findAndModifyValue();
+
+		assertThat(result.get()).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", "Han");
+	}
+
 	@Test // DATAMONGO-1563
 	void upsert() {
 
@@ -282,6 +293,19 @@ void findAndReplaceWithProjection() {
 		assertThat(result.getName()).isEqualTo(han.firstname);
 	}
 
+	@Test // GH-4949
+	void findAndReplaceWithResultConverter() {
+
+		Person luke = new Person();
+		luke.firstname = "Luke";
+
+		Optional<Jedi> result = template.update(Person.class).matching(queryHan()).replaceWith(luke).as(Jedi.class) //
+				.map((raw, converted) -> Optional.of(converted.get()))
+			.findAndReplaceValue();
+
+		assertThat(result.get()).isInstanceOf(Jedi.class).extracting(Jedi::getName).isEqualTo(han.firstname);
+	}
+
 	private Query queryHan() {
 		return query(where("id").is(han.getId()));
 	}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreatorUnitTests.java
index d18ed6f119..adaecad5da 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreatorUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreatorUnitTests.java
@@ -15,7 +15,8 @@
  */
 package org.springframework.data.mongodb.core;
 
-import static org.springframework.data.mongodb.test.util.Assertions.*;
+import static org.springframework.data.mongodb.test.util.Assertions.assertThat;
+import static org.springframework.data.mongodb.test.util.Assertions.assertThatExceptionOfType;
 
 import java.util.Collections;
 import java.util.Date;
@@ -38,6 +39,8 @@
 import org.springframework.data.mongodb.core.mapping.FieldType;
 import org.springframework.data.mongodb.core.mapping.MongoId;
 import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
+import org.springframework.data.mongodb.core.mapping.Queryable;
+import org.springframework.data.mongodb.core.mapping.RangeEncrypted;
 import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type;
 import org.springframework.data.mongodb.core.schema.JsonSchemaProperty;
 import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
@@ -282,6 +285,48 @@ void wrapEncryptedEntityTypeLikeProperty() {
 				.containsEntry("properties.domainTypeValue", Document.parse("{'encrypt': {'bsonType': 'object' } }"));
 	}
 
+	@Test // GH-4185
+	void qeRangeEncryptedProperties() {
+
+		MongoJsonSchema schema = MongoJsonSchemaCreator.create() //
+				.filter(MongoJsonSchemaCreator.encryptedOnly()) // filter non encrypted fields
+				.createSchemaFor(QueryableEncryptedRoot.class);
+
+		String expectedForInt = """
+				{ 'encrypt' : {
+					'algorithm' : 'Range',
+					'bsonType' : 'int',
+					'queries' : [
+						{ 'queryType' : 'range', 'contention' : { '$numberLong' : '0' }, 'max' : 200, 'min' : 0, 'sparsity' : 1, 'trimFactor' : 1 }
+					]
+				}}""";
+
+		String expectedForRootLong = """
+				{ 'encrypt' : {
+					'algorithm' : 'Range',
+					'bsonType' : 'long',
+					'queries' : [
+						{ 'queryType' : 'range', contention : { '$numberLong' : '0' }, 'sparsity' : 0 }
+					]
+				}}""";
+
+		String expectedForNestedLong = """
+				{ 'encrypt' : {
+					'algorithm' : 'Range',
+					'bsonType' : 'long',
+					'queries' : [
+						{ 'queryType' : 'range', contention : { '$numberLong' : '1' }, 'max' : { '$numberLong' : '1' }, 'min' : { '$numberLong' : '-1' }, 'sparsity' : 1, 'trimFactor' : 1 }
+					]
+				}}""";
+
+		assertThat(schema.schemaDocument()) //
+				.doesNotContainKey("properties.unencrypted") //
+				.containsEntry("properties.encryptedInt", Document.parse(expectedForInt))
+				.containsEntry("properties.encryptedLong", Document.parse(expectedForRootLong))
+				.containsEntry("properties.nested.properties.encrypted_long", Document.parse(expectedForNestedLong));
+
+	}
+
 	// --> TYPES AND JSON
 
 	// --> ENUM
@@ -311,7 +356,8 @@ enum JustSomeEnum {
 			"        'binaryDataProperty' : { 'bsonType' : 'binData' }," + //
 			"        'collectionProperty' : { 'type' : 'array' }," + //
 			"        'simpleTypeCollectionProperty' : { 'type' : 'array', 'items' : { 'type' : 'string' } }," + //
-			"        'complexTypeCollectionProperty' : { 'type' : 'array', 'items' : { 'type' : 'object', 'properties' : { 'field' : { 'type' : 'string'} } } }" + //
+			"        'complexTypeCollectionProperty' : { 'type' : 'array', 'items' : { 'type' : 'object', 'properties' : { 'field' : { 'type' : 'string'} } } }"
+			+ //
 			"        'enumTypeCollectionProperty' : { 'type' : 'array', 'items' : " + JUST_SOME_ENUM + " }" + //
 			"        'mapProperty' : { 'type' : 'object' }," + //
 			"        'objectProperty' : { 'type' : 'object' }," + //
@@ -692,4 +738,28 @@ static class PropertyClashWithA {
 	static class WithEncryptedEntityLikeProperty {
 		@Encrypted SomeDomainType domainTypeValue;
 	}
+
+	static class QueryableEncryptedRoot {
+
+		String unencrypted;
+
+		@RangeEncrypted(contentionFactor = 0L, rangeOptions = "{ 'min': 0, 'max': 200, 'trimFactor': 1, 'sparsity': 1}") //
+		Integer encryptedInt;
+
+		@Encrypted(algorithm = "Range")
+		@Queryable(contentionFactor = 0L, queryType = "range", queryAttributes = "{ 'sparsity': 0 }") //
+		Long encryptedLong;
+
+		NestedRangeEncrypted nested;
+
+	}
+
+	static class NestedRangeEncrypted {
+
+		@Field("encrypted_long")
+		@RangeEncrypted(contentionFactor = 1L,
+				rangeOptions = "{ 'min': { '$numberLong' : '-1' }, 'max': { '$numberLong' : '1' }, 'trimFactor': 1, 'sparsity': 1}") //
+		Long encryptedLong;
+	}
+
 }
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java
index 9730e61e51..bdc151ec63 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java
@@ -18,6 +18,7 @@
 import static org.assertj.core.api.Assertions.*;
 
 import org.bson.BsonDocument;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
@@ -31,7 +32,6 @@
 import org.springframework.data.mongodb.ClientSessionException;
 import org.springframework.data.mongodb.MongoTransactionException;
 import org.springframework.data.mongodb.UncategorizedMongoDbException;
-import org.springframework.lang.Nullable;
 
 import com.mongodb.MongoCursorNotFoundException;
 import com.mongodb.MongoException;
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateDocumentReferenceTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateDocumentReferenceTests.java
index 51b3b005a5..9a6bbb4f29 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateDocumentReferenceTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateDocumentReferenceTests.java
@@ -29,6 +29,7 @@
 
 import org.bson.Document;
 import org.bson.types.ObjectId;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
@@ -48,7 +49,6 @@
 import org.springframework.data.mongodb.test.util.Client;
 import org.springframework.data.mongodb.test.util.MongoClientExtension;
 import org.springframework.data.mongodb.test.util.MongoTestTemplate;
-import org.springframework.lang.Nullable;
 
 import com.mongodb.client.MongoClient;
 import com.mongodb.client.model.Filters;
@@ -1936,9 +1936,8 @@ public String toString() {
 
 	static class ReferencableConverter implements Converter<ReferenceAble, DocumentPointer<Object>> {
 
-		@Nullable
 		@Override
-		public DocumentPointer<Object> convert(ReferenceAble source) {
+		public @Nullable DocumentPointer<Object> convert(ReferenceAble source) {
 			return source::toReference;
 		}
 	}
@@ -1947,9 +1946,8 @@ public DocumentPointer<Object> convert(ReferenceAble source) {
 	static class DocumentToSimpleObjectRefWithReadingConverter
 			implements Converter<DocumentPointer<Document>, SimpleObjectRefWithReadingConverter> {
 
-		@Nullable
 		@Override
-		public SimpleObjectRefWithReadingConverter convert(DocumentPointer<Document> source) {
+		public @Nullable SimpleObjectRefWithReadingConverter convert(DocumentPointer<Document> source) {
 
 			Document document = client.getDatabase(DB_NAME).getCollection("simple-object-ref")
 					.find(Filters.eq("_id", source.getPointer().get("ref-key-from-custom-write-converter"))).first();
@@ -1961,9 +1959,8 @@ public SimpleObjectRefWithReadingConverter convert(DocumentPointer<Document> sou
 	static class SimpleObjectRefWithReadingConverterToDocumentConverter
 			implements Converter<SimpleObjectRefWithReadingConverter, DocumentPointer<Document>> {
 
-		@Nullable
 		@Override
-		public DocumentPointer<Document> convert(SimpleObjectRefWithReadingConverter source) {
+		public @Nullable DocumentPointer<Document> convert(SimpleObjectRefWithReadingConverter source) {
 			return () -> new Document("ref-key-from-custom-write-converter", source.getId());
 		}
 	}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateScrollTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateScrollTests.java
index 766929c732..772392f037 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateScrollTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateScrollTests.java
@@ -26,6 +26,7 @@
 import java.util.stream.Stream;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -47,7 +48,6 @@
 import org.springframework.data.mongodb.test.util.Client;
 import org.springframework.data.mongodb.test.util.MongoClientExtension;
 import org.springframework.data.mongodb.test.util.MongoTestTemplate;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ObjectUtils;
 
 import com.mongodb.client.MongoClient;
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java
index 83d4e30cc5..5a006bebfe 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java
@@ -34,7 +34,9 @@
 import java.util.stream.IntStream;
 import java.util.stream.Stream;
 
+import org.bson.Document;
 import org.bson.types.ObjectId;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -48,7 +50,7 @@
 import org.springframework.dao.OptimisticLockingFailureException;
 import org.springframework.data.annotation.Id;
 import org.springframework.data.annotation.LastModifiedDate;
-import org.springframework.data.annotation.PersistenceConstructor;
+import org.springframework.data.annotation.PersistenceCreator;
 import org.springframework.data.annotation.Version;
 import org.springframework.data.auditing.IsNewAwareAuditingHandler;
 import org.springframework.data.domain.PageRequest;
@@ -90,7 +92,6 @@
 import org.springframework.data.mongodb.test.util.MongoTestTemplate;
 import org.springframework.data.mongodb.test.util.MongoTestUtils;
 import org.springframework.data.mongodb.test.util.MongoVersion;
-import org.springframework.lang.Nullable;
 import org.springframework.test.annotation.DirtiesContext;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.ObjectUtils;
@@ -3110,6 +3111,18 @@ public void generatesIdForInsertAll() {
 		assertThat(jesse.getId()).isNotNull();
 	}
 
+	@Test // GH-4944
+	public void insertAllShouldConvertIdToTargetTypeBeforeSave() {
+
+		RawStringId walter = new RawStringId();
+		walter.value = "walter";
+
+		RawStringId returned = template.insertAll(List.of(walter)).iterator().next();
+		org.bson.Document document = template.execute(RawStringId.class, collection -> collection.find().first());
+
+		assertThat(returned.id).isEqualTo(document.get("_id"));
+	}
+
 	@Test // DATAMONGO-1208
 	public void takesSortIntoAccountWhenStreaming() {
 
@@ -4469,7 +4482,7 @@ static class TestClass {
 
 		LocalDateTime myDate;
 
-		@PersistenceConstructor
+		@PersistenceCreator
 		TestClass(LocalDateTime myDate) {
 			this.myDate = myDate;
 		}
@@ -4726,7 +4739,7 @@ static class DocumentWithLazyDBrefUsedInPresistenceConstructor {
 		@org.springframework.data.mongodb.core.mapping.DBRef(lazy = true) Document refToDocUsedInCtor;
 		@org.springframework.data.mongodb.core.mapping.DBRef(lazy = true) Document refToDocNotUsedInCtor;
 
-		@PersistenceConstructor
+		@PersistenceCreator
 		public DocumentWithLazyDBrefUsedInPresistenceConstructor(Document refToDocUsedInCtor) {
 			this.refToDocUsedInCtor = refToDocUsedInCtor;
 		}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java
index 79a0bb1fcb..ef72548fac 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java
@@ -39,6 +39,7 @@
 import org.bson.Document;
 import org.bson.conversions.Bson;
 import org.bson.types.ObjectId;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
@@ -99,7 +100,6 @@
 import org.springframework.data.mongodb.core.timeseries.Granularity;
 import org.springframework.data.mongodb.util.BsonUtils;
 import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
-import org.springframework.lang.Nullable;
 import org.springframework.mock.env.MockEnvironment;
 import org.springframework.test.util.ReflectionTestUtils;
 import org.springframework.util.CollectionUtils;
@@ -437,8 +437,8 @@ void findAllAndRemoveShouldRemoveDocumentsReturedByFindQuery() {
 
 		verify(collection, times(1)).deleteMany(queryCaptor.capture(), any());
 
-		Document idField = DocumentTestUtils.getAsDocument(queryCaptor.getValue(), "_id");
-		assertThat((List<Object>) idField.get("$in")).containsExactly(Integer.valueOf(0), Integer.valueOf(1));
+		List<Document> ors = DocumentTestUtils.getAsDBList(queryCaptor.getValue(), "$or");
+		assertThat(ors).containsExactlyInAnyOrder(new Document("_id", 0), new Document("_id", 1));
 	}
 
 	@Test // DATAMONGO-566
@@ -1156,7 +1156,7 @@ void countShouldApplyQueryHintAsIndexNameIfPresent() {
 	void appliesFieldsWhenInterfaceProjectionIsClosedAndQueryDoesNotDefineFields() {
 
 		template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document(), Person.class,
-				PersonProjection.class, CursorPreparer.NO_OP_PREPARER);
+				PersonProjection.class, QueryResultConverter.entity(), CursorPreparer.NO_OP_PREPARER);
 
 		verify(findIterable).projection(eq(new Document("firstname", 1)));
 	}
@@ -1165,7 +1165,7 @@ void appliesFieldsWhenInterfaceProjectionIsClosedAndQueryDoesNotDefineFields() {
 	void doesNotApplyFieldsWhenInterfaceProjectionIsClosedAndQueryDefinesFields() {
 
 		template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document("bar", 1), Person.class,
-				PersonProjection.class, CursorPreparer.NO_OP_PREPARER);
+				PersonProjection.class, QueryResultConverter.entity(), CursorPreparer.NO_OP_PREPARER);
 
 		verify(findIterable).projection(eq(new Document("bar", 1)));
 	}
@@ -1174,7 +1174,7 @@ void doesNotApplyFieldsWhenInterfaceProjectionIsClosedAndQueryDefinesFields() {
 	void doesNotApplyFieldsWhenInterfaceProjectionIsOpen() {
 
 		template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document(), Person.class,
-				PersonSpELProjection.class, CursorPreparer.NO_OP_PREPARER);
+				PersonSpELProjection.class, QueryResultConverter.entity(), CursorPreparer.NO_OP_PREPARER);
 
 		verify(findIterable).projection(eq(BsonUtils.EMPTY_DOCUMENT));
 	}
@@ -1183,7 +1183,7 @@ void doesNotApplyFieldsWhenInterfaceProjectionIsOpen() {
 	void appliesFieldsToDtoProjection() {
 
 		template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document(), Person.class,
-				Jedi.class, CursorPreparer.NO_OP_PREPARER);
+				Jedi.class, QueryResultConverter.entity(), CursorPreparer.NO_OP_PREPARER);
 
 		verify(findIterable).projection(eq(new Document("firstname", 1)));
 	}
@@ -1192,7 +1192,7 @@ void appliesFieldsToDtoProjection() {
 	void doesNotApplyFieldsToDtoProjectionWhenQueryDefinesFields() {
 
 		template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document("bar", 1), Person.class,
-				Jedi.class, CursorPreparer.NO_OP_PREPARER);
+				Jedi.class, QueryResultConverter.entity(), CursorPreparer.NO_OP_PREPARER);
 
 		verify(findIterable).projection(eq(new Document("bar", 1)));
 	}
@@ -1201,7 +1201,7 @@ void doesNotApplyFieldsToDtoProjectionWhenQueryDefinesFields() {
 	void doesNotApplyFieldsWhenTargetIsNotAProjection() {
 
 		template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document(), Person.class,
-				Person.class, CursorPreparer.NO_OP_PREPARER);
+				Person.class, QueryResultConverter.entity(), CursorPreparer.NO_OP_PREPARER);
 
 		verify(findIterable).projection(eq(BsonUtils.EMPTY_DOCUMENT));
 	}
@@ -1210,7 +1210,7 @@ void doesNotApplyFieldsWhenTargetIsNotAProjection() {
 	void doesNotApplyFieldsWhenTargetExtendsDomainType() {
 
 		template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document(), Person.class,
-				PersonExtended.class, CursorPreparer.NO_OP_PREPARER);
+				PersonExtended.class, QueryResultConverter.entity(), CursorPreparer.NO_OP_PREPARER);
 
 		verify(findIterable).projection(eq(BsonUtils.EMPTY_DOCUMENT));
 	}
@@ -2908,8 +2908,7 @@ public List<T> getValues() {
 			return values;
 		}
 
-		@Nullable
-		public T getValue() {
+		public @Nullable T getValue() {
 			return CollectionUtils.lastElement(values);
 		}
 
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateValidationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateValidationTests.java
index 18da8c516d..fd1b70f3c7 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateValidationTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateValidationTests.java
@@ -25,6 +25,7 @@
 import java.util.Set;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -39,7 +40,6 @@
 import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
 import org.springframework.data.mongodb.test.util.Client;
 import org.springframework.data.mongodb.test.util.MongoClientExtension;
-import org.springframework.lang.Nullable;
 import org.springframework.test.context.junit.jupiter.SpringExtension;
 
 import com.mongodb.client.MongoClient;
@@ -242,13 +242,11 @@ public SimpleBean(@Nullable String nonNullString, @Nullable Integer rangedIntege
 			this.customFieldName = customFieldName;
 		}
 
-		@Nullable
-		public String getNonNullString() {
+		public @Nullable String getNonNullString() {
 			return this.nonNullString;
 		}
 
-		@Nullable
-		public Integer getRangedInteger() {
+		public @Nullable Integer getRangedInteger() {
 			return this.rangedInteger;
 		}
 
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/Person.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/Person.java
index bc126e05f0..7b07cd9448 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/Person.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/Person.java
@@ -16,7 +16,7 @@
 package org.springframework.data.mongodb.core;
 
 import org.bson.types.ObjectId;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 public class Person {
 
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryResultConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryResultConverterUnitTests.java
new file mode 100644
index 0000000000..107b942161
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryResultConverterUnitTests.java
@@ -0,0 +1,92 @@
+/*
+ * 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.mongodb.core;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.bson.Document;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.data.mongodb.core.QueryResultConverter.ConversionResultSupplier;
+
+/**
+ * Unit tests for {@link QueryResultConverter}.
+ *
+ * @author Christoph Strobl
+ */
+class QueryResultConverterUnitTests {
+
+	public static final ConversionResultSupplier<Document> ERROR_SUPPLIER = () -> {
+		throw new IllegalStateException("must not read conversion result");
+	};
+
+	@Test // GH-4949
+	void converterDoesNotEagerlyRetrieveConversionResultFromSupplier() {
+
+		QueryResultConverter<Document, String> converter = new QueryResultConverter<Document, String>() {
+
+			@Override
+			public String mapDocument(Document document, ConversionResultSupplier<Document> reader) {
+				return "done";
+			}
+		};
+
+		assertThat(converter.mapDocument(new Document(), ERROR_SUPPLIER)).isEqualTo("done");
+	}
+
+	@Test // GH-4949
+	void converterPassesOnConversionResultToNextStage() {
+
+		Document source = new Document("value", "10");
+
+		QueryResultConverter<Document, Integer> stagedConverter = new QueryResultConverter<Document, String>() {
+
+			@Override
+			public String mapDocument(Document document, ConversionResultSupplier<Document> reader) {
+				return document.get("value", "-1");
+			}
+		}.andThen(new QueryResultConverter<String, Integer>() {
+
+			@Override
+			public Integer mapDocument(Document document, ConversionResultSupplier<String> reader) {
+
+				assertThat(document).isEqualTo(source);
+				return Integer.valueOf(reader.get());
+			}
+		});
+
+		assertThat(stagedConverter.mapDocument(source, ERROR_SUPPLIER)).isEqualTo(10);
+	}
+
+	@Test // GH-4949
+	void entityConverterDelaysConversion() {
+
+		Document source = new Document("value", "10");
+
+		QueryResultConverter<Document, Integer> converter = QueryResultConverter.<Document> entity()
+				.andThen(new QueryResultConverter<Document, Integer>() {
+
+					@Override
+					public Integer mapDocument(Document document, ConversionResultSupplier<Document> reader) {
+
+						assertThat(document).isEqualTo(source);
+						return Integer.valueOf(document.get("value", "20"));
+					}
+				});
+
+		assertThat(converter.mapDocument(source, ERROR_SUPPLIER)).isEqualTo(10);
+	}
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupportUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupportUnitTests.java
index 9d4ed339b5..83e1b3c272 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupportUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupportUnitTests.java
@@ -72,7 +72,8 @@ void aggregateWithUntypedAggregationAndExplicitCollection() {
 		opSupport.aggregateAndReturn(Person.class).inCollection("star-wars").by(newAggregation(project("foo"))).all();
 
 		ArgumentCaptor<Class> captor = ArgumentCaptor.forClass(Class.class);
-		verify(template).aggregate(any(Aggregation.class), eq("star-wars"), captor.capture());
+		verify(template).doAggregate(any(Aggregation.class), eq("star-wars"), captor.capture(), any(Class.class),
+				eq(QueryResultConverter.entity()));
 		assertThat(captor.getValue()).isEqualTo(Person.class);
 	}
 
@@ -86,7 +87,8 @@ void aggregateWithUntypedAggregation() {
 		ArgumentCaptor<Class> captor = ArgumentCaptor.forClass(Class.class);
 
 		verify(template).getCollectionName(captor.capture());
-		verify(template).aggregate(any(Aggregation.class), eq("person"), captor.capture());
+		verify(template).doAggregate(any(Aggregation.class), eq("person"), captor.capture(), any(Class.class),
+				eq(QueryResultConverter.entity()));
 
 		assertThat(captor.getAllValues()).containsExactly(Person.class, Person.class);
 	}
@@ -101,7 +103,8 @@ void aggregateWithTypeAggregation() {
 		ArgumentCaptor<Class> captor = ArgumentCaptor.forClass(Class.class);
 
 		verify(template).getCollectionName(captor.capture());
-		verify(template).aggregate(any(Aggregation.class), eq("person"), captor.capture());
+		verify(template).doAggregate(any(Aggregation.class), eq("person"), captor.capture(), any(Class.class),
+				eq(QueryResultConverter.entity()));
 
 		assertThat(captor.getAllValues()).containsExactly(Person.class, Jedi.class);
 	}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupportTests.java
index f23e973202..d51696dd74 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupportTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupportTests.java
@@ -26,6 +26,7 @@
 
 import java.util.Date;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
@@ -167,6 +168,17 @@ void findAllWithProjection() {
 				.verifyComplete();
 	}
 
+	@Test // GH-4949
+	void findAllWithConverter() {
+
+		template.query(Person.class).as(Jedi.class).map((document, reader) -> Optional.of(reader.get())).all()
+				.map(Optional::get) //
+				.map(it -> it.getClass().getName()) //
+				.as(StepVerifier::create) //
+				.expectNext(Jedi.class.getName(), Jedi.class.getName()) //
+				.verifyComplete();
+	}
+
 	@Test // DATAMONGO-1719
 	void findAllBy() {
 
@@ -299,6 +311,32 @@ void findAllNearByWithCollectionAndProjection() {
 				.verifyComplete();
 	}
 
+	@Test // GH-4949
+	@DirtiesState
+	void findAllNearByWithConverter() {
+
+		blocking.indexOps(Planet.class).ensureIndex(
+				new GeospatialIndex("coordinates").typed(GeoSpatialIndexType.GEO_2DSPHERE).named("planet-coordinate-idx"));
+
+		Planet alderan = new Planet("alderan", new Point(-73.9836, 40.7538));
+		Planet dantooine = new Planet("dantooine", new Point(-73.9928, 40.7193));
+
+		blocking.save(alderan);
+		blocking.save(dantooine);
+
+		template.query(Object.class).inCollection(STAR_WARS).as(Human.class)
+				.near(NearQuery.near(-73.9667, 40.78).spherical(true)).map((document, reader) -> Optional.of(reader.get())) //
+				.all() //
+				.as(StepVerifier::create).consumeNextWith(actual -> {
+					assertThat(actual.getDistance()).isNotNull();
+					assertThat(actual.getContent()).isInstanceOf(Optional.class);
+					assertThat(actual.getContent().get()).isInstanceOf(Human.class);
+					assertThat(actual.getContent().get().getId()).isEqualTo("alderan");
+				}) //
+				.expectNextCount(1) //
+				.verifyComplete();
+	}
+
 	@Test // DATAMONGO-1719
 	@DirtiesState
 	void findAllNearByReturningGeoResultContentAsClosedInterfaceProjection() {
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupportUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupportUnitTests.java
index 609a456912..b5a40f5738 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupportUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupportUnitTests.java
@@ -74,7 +74,7 @@ void usesExtractedCollectionName() {
 		mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION).all();
 
 		verify(template).mapReduce(any(Query.class), eq(Person.class), eq(STAR_WARS), eq(Person.class), eq(MAP_FUNCTION),
-				eq(REDUCE_FUNCTION), isNull());
+				eq(REDUCE_FUNCTION), any());
 	}
 
 	@Test // DATAMONGO-1929
@@ -84,7 +84,7 @@ void usesExplicitCollectionName() {
 				.inCollection("the-night-angel").all();
 
 		verify(template).mapReduce(any(Query.class), eq(Person.class), eq("the-night-angel"), eq(Person.class),
-				eq(MAP_FUNCTION), eq(REDUCE_FUNCTION), isNull());
+				eq(MAP_FUNCTION), eq(REDUCE_FUNCTION), any());
 	}
 
 	@Test // DATAMONGO-1929
@@ -108,7 +108,7 @@ void usesQueryWhenPresent() {
 		mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION).matching(query).all();
 
 		verify(template).mapReduce(eq(query), eq(Person.class), eq(STAR_WARS), eq(Person.class), eq(MAP_FUNCTION),
-				eq(REDUCE_FUNCTION), isNull());
+				eq(REDUCE_FUNCTION), any());
 	}
 
 	@Test // DATAMONGO-2416
@@ -121,7 +121,7 @@ void usesCriteriaWhenPresent() {
 				.matching(where("lastname").is("skywalker")).all();
 
 		verify(template).mapReduce(eq(query), eq(Person.class), eq(STAR_WARS), eq(Person.class), eq(MAP_FUNCTION),
-				eq(REDUCE_FUNCTION), isNull());
+				eq(REDUCE_FUNCTION), any());
 	}
 
 	@Test // DATAMONGO-1929
@@ -132,7 +132,7 @@ void usesProjectionWhenPresent() {
 		mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION).as(Jedi.class).all();
 
 		verify(template).mapReduce(any(Query.class), eq(Person.class), eq(STAR_WARS), eq(Jedi.class), eq(MAP_FUNCTION),
-				eq(REDUCE_FUNCTION), isNull());
+				eq(REDUCE_FUNCTION), any());
 	}
 
 	interface Contact {}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java
index 80dd584b9e..f87227cdde 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java
@@ -48,6 +48,7 @@
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
+
 import org.springframework.context.ConfigurableApplicationContext;
 import org.springframework.context.support.GenericApplicationContext;
 import org.springframework.dao.DataIntegrityViolationException;
@@ -84,6 +85,7 @@
 
 import com.mongodb.WriteConcern;
 import com.mongodb.reactivestreams.client.MongoClient;
+import com.mongodb.reactivestreams.client.MongoCollection;
 
 /**
  * Integration test for {@link MongoTemplate}.
@@ -165,6 +167,19 @@ void insertCollectionSetsId() {
 		assertThat(person.getId()).isNotNull();
 	}
 
+	@Test // GH-4944
+	void insertAllShouldConvertIdToTargetTypeBeforeSave() {
+
+		RawStringId walter = new RawStringId();
+		walter.value = "walter";
+
+		RawStringId returned = template.insertAll(List.of(walter)).blockLast();
+		template.execute(RawStringId.class, MongoCollection::find) //
+				.as(StepVerifier::create) //
+				.consumeNextWith(actual -> assertThat(returned.id).isEqualTo(actual.get("_id"))) //
+				.verifyComplete();
+	}
+
 	@Test // DATAMONGO-1444
 	void saveSetsId() {
 
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java
index f89b2fa8c1..36cf0886ad 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java
@@ -18,6 +18,7 @@
 import static org.assertj.core.api.Assertions.*;
 import static org.mockito.Mockito.*;
 import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
+import static org.springframework.data.mongodb.test.util.Assertions.*;
 import static org.springframework.data.mongodb.test.util.Assertions.assertThat;
 
 import reactor.core.publisher.Flux;
@@ -43,6 +44,7 @@
 import org.bson.Document;
 import org.bson.conversions.Bson;
 import org.bson.types.ObjectId;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -53,6 +55,7 @@
 import org.mockito.quality.Strictness;
 import org.reactivestreams.Publisher;
 import org.reactivestreams.Subscriber;
+
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.ApplicationListener;
@@ -93,7 +96,6 @@
 import org.springframework.data.mongodb.core.timeseries.Granularity;
 import org.springframework.data.mongodb.util.BsonUtils;
 import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
-import org.springframework.lang.Nullable;
 import org.springframework.test.util.ReflectionTestUtils;
 import org.springframework.util.CollectionUtils;
 
@@ -437,7 +439,7 @@ void geoNearShouldHonorReadConcernFromQuery() {
 	void appliesFieldsWhenInterfaceProjectionIsClosedAndQueryDoesNotDefineFields() {
 
 		template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document(), Person.class,
-				PersonProjection.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe();
+				PersonProjection.class, QueryResultConverter.entity(), FindPublisherPreparer.NO_OP_PREPARER).subscribe();
 
 		verify(findPublisher).projection(eq(new Document("firstname", 1)));
 	}
@@ -446,7 +448,7 @@ void appliesFieldsWhenInterfaceProjectionIsClosedAndQueryDoesNotDefineFields() {
 	void doesNotApplyFieldsWhenInterfaceProjectionIsClosedAndQueryDefinesFields() {
 
 		template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document("bar", 1), Person.class,
-				PersonProjection.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe();
+				PersonProjection.class, QueryResultConverter.entity(), FindPublisherPreparer.NO_OP_PREPARER).subscribe();
 
 		verify(findPublisher).projection(eq(new Document("bar", 1)));
 	}
@@ -455,7 +457,7 @@ void doesNotApplyFieldsWhenInterfaceProjectionIsClosedAndQueryDefinesFields() {
 	void doesNotApplyFieldsWhenInterfaceProjectionIsOpen() {
 
 		template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document(), Person.class,
-				PersonSpELProjection.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe();
+				PersonSpELProjection.class, QueryResultConverter.entity(), FindPublisherPreparer.NO_OP_PREPARER).subscribe();
 
 		verify(findPublisher, never()).projection(any());
 	}
@@ -464,7 +466,7 @@ void doesNotApplyFieldsWhenInterfaceProjectionIsOpen() {
 	void appliesFieldsToDtoProjection() {
 
 		template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document(), Person.class,
-				Jedi.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe();
+				Jedi.class, QueryResultConverter.entity(), FindPublisherPreparer.NO_OP_PREPARER).subscribe();
 
 		verify(findPublisher).projection(eq(new Document("firstname", 1)));
 	}
@@ -473,7 +475,7 @@ void appliesFieldsToDtoProjection() {
 	void doesNotApplyFieldsToDtoProjectionWhenQueryDefinesFields() {
 
 		template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document("bar", 1), Person.class,
-				Jedi.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe();
+				Jedi.class, QueryResultConverter.entity(), FindPublisherPreparer.NO_OP_PREPARER).subscribe();
 
 		verify(findPublisher).projection(eq(new Document("bar", 1)));
 	}
@@ -482,7 +484,7 @@ void doesNotApplyFieldsToDtoProjectionWhenQueryDefinesFields() {
 	void doesNotApplyFieldsWhenTargetIsNotAProjection() {
 
 		template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document(), Person.class,
-				Person.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe();
+				Person.class, QueryResultConverter.entity(), FindPublisherPreparer.NO_OP_PREPARER).subscribe();
 
 		verify(findPublisher, never()).projection(any());
 	}
@@ -491,7 +493,7 @@ void doesNotApplyFieldsWhenTargetIsNotAProjection() {
 	void doesNotApplyFieldsWhenTargetExtendsDomainType() {
 
 		template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document(), Person.class,
-				PersonExtended.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe();
+				PersonExtended.class, QueryResultConverter.entity(), FindPublisherPreparer.NO_OP_PREPARER).subscribe();
 
 		verify(findPublisher, never()).projection(any());
 	}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupportTests.java
index 5659869705..cfdc5fe1a1 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupportTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupportTests.java
@@ -15,13 +15,14 @@
  */
 package org.springframework.data.mongodb.core;
 
-import static org.assertj.core.api.Assertions.*;
-import static org.springframework.data.mongodb.core.query.Criteria.*;
-import static org.springframework.data.mongodb.core.query.Query.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.data.mongodb.core.query.Criteria.where;
+import static org.springframework.data.mongodb.core.query.Query.query;
 
 import reactor.test.StepVerifier;
 
 import java.util.Objects;
+import java.util.Optional;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -108,6 +109,15 @@ void removeAndReturnAllMatching() {
 				.expectNext(han).verifyComplete();
 	}
 
+	@Test // GH-4949
+	void removeConvertAndReturnAllMatching() {
+
+		template.remove(Person.class).matching(query(where("firstname").is("han"))).map((raw, it) -> Optional.of(it.get()))
+				.findAndRemove().as(StepVerifier::create).expectNext(Optional.of(han)).verifyComplete();
+
+		template.findById(han.id, Person.class).as(StepVerifier::create).verifyComplete();
+	}
+
 	@org.springframework.data.mongodb.core.mapping.Document(collection = STAR_WARS)
 	static class Person {
 
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java
index 73970d2ad3..02637e9971 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java
@@ -21,6 +21,7 @@
 
 import java.lang.reflect.Proxy;
 
+import com.mongodb.reactivestreams.client.ListCollectionNamesPublisher;
 import org.bson.Document;
 import org.bson.codecs.BsonValueCodec;
 import org.bson.codecs.configuration.CodecRegistry;
@@ -58,7 +59,6 @@
 import com.mongodb.reactivestreams.client.MongoClient;
 import com.mongodb.reactivestreams.client.MongoCollection;
 import com.mongodb.reactivestreams.client.MongoDatabase;
-import org.springframework.data.mongodb.util.MongoCompatibilityAdapter;
 
 /**
  * Unit tests for {@link ReactiveSessionBoundMongoTemplate}.
@@ -94,7 +94,7 @@ public class ReactiveSessionBoundMongoTemplateUnitTests {
 	@Before
 	public void setUp() {
 
-		mock(MongoCompatibilityAdapter.reactiveMongoDatabaseAdapter().forDb(database).collectionNamePublisherType());
+		mock(ListCollectionNamesPublisher.class);
 		when(client.getDatabase(anyString())).thenReturn(database);
 		when(codecRegistry.get(any(Class.class))).thenReturn(new BsonValueCodec());
 		when(database.getCodecRegistry()).thenReturn(codecRegistry);
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupportTests.java
index 3ac99c2b6d..bef67501b3 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupportTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupportTests.java
@@ -15,13 +15,15 @@
  */
 package org.springframework.data.mongodb.core;
 
-import static org.assertj.core.api.Assertions.*;
-import static org.springframework.data.mongodb.core.query.Criteria.*;
-import static org.springframework.data.mongodb.core.query.Query.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.springframework.data.mongodb.core.query.Criteria.where;
+import static org.springframework.data.mongodb.core.query.Query.query;
 
 import reactor.test.StepVerifier;
 
 import java.util.Objects;
+import java.util.Optional;
 
 import org.bson.BsonString;
 import org.junit.jupiter.api.BeforeEach;
@@ -175,6 +177,18 @@ void findAndModifyWithDifferentDomainTypeAndCollection() {
 				"Han");
 	}
 
+	@Test // GH-4949
+	void findAndModifyWithWithResultConversion() {
+
+		template.update(Jedi.class).inCollection(STAR_WARS).matching(query(where("_id").is(han.getId())))
+				.apply(new Update().set("name", "Han")).map((raw, it) -> Optional.of(it.get())).findAndModify()
+				.as(StepVerifier::create).consumeNextWith(actual -> assertThat(actual.get().getName()).isEqualTo("han"))
+				.verifyComplete();
+
+		assertThat(blocking.findOne(queryHan(), Person.class)).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname",
+				"Han");
+	}
+
 	@Test // DATAMONGO-1719
 	void findAndModifyWithOptions() {
 
@@ -225,6 +239,18 @@ void findAndReplaceWithProjection() {
 				}).verifyComplete();
 	}
 
+	@Test // GH-4949
+	void findAndReplaceWithResultConversion() {
+
+		Person luke = new Person();
+		luke.firstname = "Luke";
+
+		template.update(Person.class).matching(queryHan()).replaceWith(luke).map((raw, it) -> Optional.of(it.get())).findAndReplace() //
+			.as(StepVerifier::create).consumeNextWith(it -> {
+				assertThat(it.get().getFirstname()).isEqualTo(han.firstname);
+			}).verifyComplete();
+	}
+
 	@Test // DATAMONGO-1827
 	void findAndReplaceWithCollection() {
 
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java
index dfa4b00515..50ea579044 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java
@@ -49,7 +49,6 @@
 import com.mongodb.client.model.DeleteOptions;
 import com.mongodb.client.model.FindOneAndUpdateOptions;
 import com.mongodb.client.model.UpdateOptions;
-import org.springframework.data.mongodb.util.MongoCompatibilityAdapter;
 
 /**
  * Unit test for {@link SessionBoundMongoTemplate} making sure a proxied {@link MongoCollection} and
@@ -90,7 +89,7 @@ public class SessionBoundMongoTemplateUnitTests {
 	@Before
 	public void setUp() {
 
-		collectionNamesIterable = mock(MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(database).collectionNameIterableType());
+		collectionNamesIterable = mock(ListCollectionNamesIterable.class);
 		when(client.getDatabase(anyString())).thenReturn(database);
 		when(codecRegistry.get(any(Class.class))).thenReturn(new BsonValueCodec());
 		when(database.getCodecRegistry()).thenReturn(codecRegistry);
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/TransactionOptionsTestService.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/TransactionOptionsTestService.java
index 8968f53a74..b8cc9cc972 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/TransactionOptionsTestService.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/TransactionOptionsTestService.java
@@ -18,7 +18,7 @@
 import java.util.function.Function;
 import java.util.function.UnaryOperator;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.transaction.annotation.Transactional;
 
 /**
@@ -48,45 +48,38 @@ public T saveWithinMaxCommitTime(T entity) {
 		return saveFunction.apply(entity);
 	}
 
-	@Nullable
 	@Transactional(transactionManager = "txManager", label = { "mongo:readConcern=available" })
-	public T availableReadConcernFind(Object id) {
+	public @Nullable T availableReadConcernFind(Object id) {
 		return findByIdFunction.apply(id);
 	}
 
-	@Nullable
 	@Transactional(transactionManager = "txManager", label = { "mongo:readConcern=invalid" })
-	public T invalidReadConcernFind(Object id) {
+	public @Nullable T invalidReadConcernFind(Object id) {
 		return findByIdFunction.apply(id);
 	}
 
-	@Nullable
 	@Transactional(transactionManager = "txManager", label = { "mongo:readConcern=${tx.read.concern}" })
-	public T environmentReadConcernFind(Object id) {
+	public @Nullable T environmentReadConcernFind(Object id) {
 		return findByIdFunction.apply(id);
 	}
 
-	@Nullable
 	@Transactional(transactionManager = "txManager", label = { "mongo:readConcern=majority" })
-	public T majorityReadConcernFind(Object id) {
+	public @Nullable T majorityReadConcernFind(Object id) {
 		return findByIdFunction.apply(id);
 	}
 
-	@Nullable
 	@Transactional(transactionManager = "txManager", label = { "mongo:readPreference=primaryPreferred" })
-	public T findFromPrimaryPreferredReplica(Object id) {
+	public @Nullable T findFromPrimaryPreferredReplica(Object id) {
 		return findByIdFunction.apply(id);
 	}
 
-	@Nullable
 	@Transactional(transactionManager = "txManager", label = { "mongo:readPreference=invalid" })
-	public T findFromInvalidReplica(Object id) {
+	public @Nullable T findFromInvalidReplica(Object id) {
 		return findByIdFunction.apply(id);
 	}
 
-	@Nullable
 	@Transactional(transactionManager = "txManager", label = { "mongo:readPreference=primary" })
-	public T findFromPrimaryReplica(Object id) {
+	public @Nullable T findFromPrimaryReplica(Object id) {
 		return findByIdFunction.apply(id);
 	}
 
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UpdateOperationsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UpdateOperationsUnitTests.java
index d4c2f37f63..61cd3ecce4 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UpdateOperationsUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UpdateOperationsUnitTests.java
@@ -20,6 +20,8 @@
 import java.util.Arrays;
 
 import org.bson.Document;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.Test;
 import org.springframework.data.mongodb.CodecRegistryProvider;
 import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
@@ -29,8 +31,6 @@
 import org.springframework.data.mongodb.core.convert.UpdateMapper;
 import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
-import org.springframework.lang.NonNull;
-import org.springframework.lang.Nullable;
 
 import com.mongodb.MongoClientSettings;
 
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/User.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/User.java
index 25fbbbcb83..e9eae082e3 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/User.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/User.java
@@ -15,7 +15,7 @@
  */
 package org.springframework.data.mongodb.core;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 public class User {
 
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/Venue.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/Venue.java
index 09a0605ed7..13aba70afa 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/Venue.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/Venue.java
@@ -19,7 +19,7 @@
 import java.util.Date;
 
 import org.springframework.data.annotation.Id;
-import org.springframework.data.annotation.PersistenceConstructor;
+import org.springframework.data.annotation.PersistenceCreator;
 import org.springframework.data.mongodb.core.mapping.Document;
 
 @Document("newyork")
@@ -30,7 +30,7 @@ public class Venue {
 	private double[] location;
 	private Date openingDate;
 
-	@PersistenceConstructor
+	@PersistenceCreator
 	Venue(String name, double[] location) {
 		super();
 		this.name = name;
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperationUnitTests.java
index 32c6d43220..3256b889d4 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperationUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperationUnitTests.java
@@ -20,13 +20,13 @@
 import java.util.List;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.Test;
 import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
 import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
 import org.springframework.data.mongodb.core.convert.QueryMapper;
 import org.springframework.data.mongodb.core.mapping.Field;
 import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
-import org.springframework.lang.Nullable;
 
 /**
  * Unit tests for {@link AddFieldsOperation}.
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java
index 99579b34a7..1495dec1c6 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java
@@ -37,6 +37,7 @@
 import java.util.Date;
 import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Scanner;
 import java.util.stream.Stream;
 
@@ -287,6 +288,60 @@ void shouldAggregateEmptyCollectionAndStream() {
 		}
 	}
 
+	@Test // GH-4949
+	void shouldAggregateAsStreamWithConverter() {
+
+		MongoCollection<Document> coll = mongoTemplate.getCollection(INPUT_COLLECTION);
+
+		coll.insertOne(createDocument("Doc1", "spring", "mongodb", "nosql"));
+		coll.insertOne(createDocument("Doc2"));
+
+		Aggregation aggregation = newAggregation(//
+				project("tags"), //
+				unwind("tags"), //
+				group("tags") //
+						.count().as("n"), //
+				project("n") //
+						.and("tag").previousOperation(), //
+				sort(DESC, "n") //
+		);
+
+		try (Stream<Optional<TagCount>> stream = mongoTemplate.aggregateAndReturn(TagCount.class)
+				.inCollection(INPUT_COLLECTION).by(aggregation).map((document, reader) -> Optional.of(reader.get())).stream()) {
+
+			List<TagCount> tagCount = stream.flatMap(Optional::stream).toList();
+
+			assertThat(tagCount).hasSize(3);
+		}
+	}
+
+	@Test // GH-4949
+	void shouldAggregateWithConverter() {
+
+		MongoCollection<Document> coll = mongoTemplate.getCollection(INPUT_COLLECTION);
+
+		coll.insertOne(createDocument("Doc1", "spring", "mongodb", "nosql"));
+		coll.insertOne(createDocument("Doc2"));
+
+		Aggregation aggregation = newAggregation(//
+				project("tags"), //
+				unwind("tags"), //
+				group("tags") //
+						.count().as("n"), //
+				project("n") //
+						.and("tag").previousOperation(), //
+				sort(DESC, "n") //
+		);
+
+		AggregationResults<Optional<TagCount>> results = mongoTemplate.aggregateAndReturn(TagCount.class)
+				.inCollection(INPUT_COLLECTION) //
+				.by(aggregation) //
+				.map((document, reader) -> Optional.of(reader.get())) //
+				.all();
+
+		assertThat(results.getMappedResults()).extracting(Optional::get).hasOnlyElementsOfType(TagCount.class).hasSize(3);
+	}
+
 	@Test // DATAMONGO-1391
 	void shouldUnwindWithIndex() {
 
@@ -501,7 +556,7 @@ void findStatesWithPopulationOver10MillionAggregationExample() {
 		/*
 		 //complex mongodb aggregation framework example from
 		 https://docs.mongodb.org/manual/tutorial/aggregation-examples/#largest-and-smallest-cities-by-state
-		
+
 		 db.zipcodes.aggregate(
 			 	{
 				   $group: {
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArrayOperatorsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArrayOperatorsUnitTests.java
index 007fdbb28c..0ab5545f23 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArrayOperatorsUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArrayOperatorsUnitTests.java
@@ -15,7 +15,7 @@
  */
 package org.springframework.data.mongodb.core.aggregation;
 
-import static org.springframework.data.mongodb.test.util.Assertions.*;
+import static org.springframework.data.mongodb.test.util.Assertions.assertThat;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -24,6 +24,7 @@
 import org.bson.Document;
 import org.junit.jupiter.api.Test;
 import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Sort.Direction;
 import org.springframework.data.mongodb.core.aggregation.ArrayOperators.ArrayToObject;
 
 /**
@@ -179,4 +180,26 @@ void sortByWithFieldRef() {
 		assertThat(ArrayOperators.arrayOf("team").sort(Sort.by("name")).toDocument(Aggregation.DEFAULT_CONTEXT))
 				.isEqualTo("{ $sortArray: { input: \"$team\", sortBy: { name: 1 } } }");
 	}
+
+	@Test // GH-4929
+	public void sortArrayByValueAscending() {
+
+		Document result = ArrayOperators.arrayOf("numbers").sort(Direction.ASC).toDocument(Aggregation.DEFAULT_CONTEXT);
+		assertThat(result).isEqualTo("{ $sortArray: { input: '$numbers', sortBy: 1 } }");
+	}
+
+	@Test // GH-4929
+	public void sortArrayByValueDescending() {
+
+		Document result = ArrayOperators.arrayOf("numbers").sort(Direction.DESC).toDocument(Aggregation.DEFAULT_CONTEXT);
+		assertThat(result).isEqualTo("{ $sortArray: { input: '$numbers', sortBy: -1 } }");
+	}
+
+	@Test // GH-4929
+	void sortByWithDirection() {
+
+		assertThat(ArrayOperators.arrayOf(List.of("a", "b", "d", "c")).sort(Direction.DESC)
+				.toDocument(Aggregation.DEFAULT_CONTEXT))
+				.isEqualTo("{ $sortArray: { input: [\"a\", \"b\", \"d\", \"c\"], sortBy: -1 } }");
+	}
 }
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DensifyOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DensifyOperationUnitTests.java
index 47176fd8ab..d830a44582 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DensifyOperationUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DensifyOperationUnitTests.java
@@ -19,6 +19,7 @@
 
 import java.util.Date;
 
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.Test;
 import org.springframework.data.mongodb.core.aggregation.DensifyOperation.DensifyUnits;
 import org.springframework.data.mongodb.core.aggregation.DensifyOperation.Range;
@@ -27,7 +28,6 @@
 import org.springframework.data.mongodb.core.convert.QueryMapper;
 import org.springframework.data.mongodb.core.mapping.Field;
 import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
-import org.springframework.lang.Nullable;
 
 /**
  * Unit tests for {@link DensifyOperation}.
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java
index 9496a51c03..5f66e61bdc 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java
@@ -20,6 +20,7 @@
 import java.util.Arrays;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.Test;
 import org.springframework.data.annotation.Id;
 import org.springframework.data.geo.Distance;
@@ -33,7 +34,6 @@
 import org.springframework.data.mongodb.core.query.Criteria;
 import org.springframework.data.mongodb.core.query.NearQuery;
 import org.springframework.data.mongodb.core.query.Query;
-import org.springframework.lang.Nullable;
 
 /**
  * Unit tests for {@link GeoNearOperation}.
@@ -70,7 +70,7 @@ public void rendersNearQueryWithKeyCorrectly() {
 	@Test // DATAMONGO-2264
 	public void rendersMaxDistanceCorrectly() {
 
-		NearQuery query = NearQuery.near(10.0, 20.0).maxDistance(new Distance(30.0));
+		NearQuery query = NearQuery.near(10.0, 20.0).maxDistance(Distance.of(30.0));
 
 		assertThat(new GeoNearOperation(query, "distance").toPipelineStages(Aggregation.DEFAULT_CONTEXT))
 				.containsExactly($geoNear().near(10.0, 20.0).maxDistance(30.0).doc());
@@ -79,7 +79,7 @@ public void rendersMaxDistanceCorrectly() {
 	@Test // DATAMONGO-2264
 	public void rendersMinDistanceCorrectly() {
 
-		NearQuery query = NearQuery.near(10.0, 20.0).minDistance(new Distance(30.0));
+		NearQuery query = NearQuery.near(10.0, 20.0).minDistance(Distance.of(30.0));
 
 		assertThat(new GeoNearOperation(query, "distance").toPipelineStages(Aggregation.DEFAULT_CONTEXT))
 				.containsExactly($geoNear().near(10.0, 20.0).minDistance(30.0).doc());
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/MergeOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/MergeOperationUnitTests.java
index 311496ba8d..18980f6a06 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/MergeOperationUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/MergeOperationUnitTests.java
@@ -23,6 +23,7 @@
 import java.util.Arrays;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.Test;
 import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Sum;
 import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
@@ -30,7 +31,6 @@
 import org.springframework.data.mongodb.core.convert.QueryMapper;
 import org.springframework.data.mongodb.core.mapping.Field;
 import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
-import org.springframework.lang.Nullable;
 
 /**
  * Unit tests for {@link MergeOperation}.
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/Order.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/Order.java
index 1174507e1c..a463b72cff 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/Order.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/Order.java
@@ -20,6 +20,8 @@
 import java.util.Date;
 import java.util.List;
 
+import org.springframework.lang.Contract;
+
 /**
  * @author Thomas Darimont
  */
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReactiveAggregationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReactiveAggregationTests.java
index 55d6bf3b60..92c1d2cd97 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReactiveAggregationTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReactiveAggregationTests.java
@@ -22,6 +22,7 @@
 import reactor.test.StepVerifier;
 
 import java.util.Arrays;
+import java.util.Optional;
 
 import org.bson.Document;
 import org.junit.After;
@@ -115,6 +116,29 @@ public void shouldProjectMultipleDocuments() {
 				}).verifyComplete();
 	}
 
+	@Test // GH-4949
+	public void shouldProjectAndConvertMultipleDocuments() {
+
+		City dresden = new City("Dresden", 100);
+		City linz = new City("Linz", 101);
+		City braunschweig = new City("Braunschweig", 102);
+		City weinheim = new City("Weinheim", 103);
+
+		reactiveMongoTemplate.insertAll(Arrays.asList(dresden, linz, braunschweig, weinheim)).as(StepVerifier::create)
+				.expectNextCount(4).verifyComplete();
+
+		Aggregation agg = newAggregation( //
+				match(where("population").lt(103)));
+
+		reactiveMongoTemplate.aggregateAndReturn(City.class).inCollection("city").by(agg)
+				.map((document, reader) -> Optional.of(reader.get())) //
+				.all() //
+				.collectList() //
+				.as(StepVerifier::create).consumeNextWith(actual -> {
+					assertThat(actual).hasSize(3).extracting(Optional::get).contains(dresden, linz, braunschweig);
+				}).verifyComplete();
+	}
+
 	@Test // DATAMONGO-1646
 	public void shouldAggregateToOutCollection() {
 
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/RedactOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/RedactOperationUnitTests.java
index 24566089e7..d29e32a988 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/RedactOperationUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/RedactOperationUnitTests.java
@@ -20,6 +20,7 @@
 import java.util.Arrays;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.Test;
 import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
 import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
@@ -27,7 +28,6 @@
 import org.springframework.data.mongodb.core.mapping.Field;
 import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
 import org.springframework.data.mongodb.core.query.Criteria;
-import org.springframework.lang.Nullable;
 
 /**
  * Unit tests for {@link RedactOperation}.
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetOperationUnitTests.java
index 093d4af7a0..82ad4c223f 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetOperationUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetOperationUnitTests.java
@@ -20,6 +20,7 @@
 import java.util.List;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.Test;
 
 import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
@@ -27,7 +28,6 @@
 import org.springframework.data.mongodb.core.convert.QueryMapper;
 import org.springframework.data.mongodb.core.mapping.Field;
 import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
-import org.springframework.lang.Nullable;
 
 /**
  * Unit tests for {@link SetOperation}.
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetWindowFieldsOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetWindowFieldsOperationUnitTests.java
index b5f5f596e6..18eb659cd0 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetWindowFieldsOperationUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetWindowFieldsOperationUnitTests.java
@@ -20,6 +20,7 @@
 import java.util.Date;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.Test;
 import org.springframework.data.domain.Sort;
 import org.springframework.data.domain.Sort.Direction;
@@ -30,7 +31,6 @@
 import org.springframework.data.mongodb.core.convert.QueryMapper;
 import org.springframework.data.mongodb.core.mapping.Field;
 import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
-import org.springframework.lang.Nullable;
 
 /**
  * Unit tests for {@link SetWindowFieldsOperation}.
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperationUnitTests.java
index e47fea289e..ef514ca882 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperationUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperationUnitTests.java
@@ -21,13 +21,13 @@
 import java.util.List;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.Test;
 import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
 import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
 import org.springframework.data.mongodb.core.convert.QueryMapper;
 import org.springframework.data.mongodb.core.mapping.Field;
 import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
-import org.springframework.lang.Nullable;
 
 /**
  * Unit tests for {@link UnionWithOperation}.
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnsetOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnsetOperationUnitTests.java
index 2f081cc9fc..c406b89626 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnsetOperationUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnsetOperationUnitTests.java
@@ -22,6 +22,7 @@
 import java.util.Collections;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.Test;
 import org.springframework.data.annotation.Id;
 import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
@@ -29,7 +30,6 @@
 import org.springframework.data.mongodb.core.convert.QueryMapper;
 import org.springframework.data.mongodb.core.mapping.Field;
 import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
-import org.springframework.lang.Nullable;
 
 /**
  * Unit tests for {@link UnsetOperation}.
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperationUnitTests.java
index 4ce045fe6f..936460f466 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperationUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperationUnitTests.java
@@ -15,14 +15,14 @@
  */
 package org.springframework.data.mongodb.core.aggregation;
 
-import static org.assertj.core.api.Assertions.*;
+import static org.springframework.data.mongodb.test.util.Assertions.assertThat;
 
 import java.util.List;
 
 import org.bson.Document;
 import org.junit.jupiter.api.Test;
-
 import org.springframework.data.annotation.Id;
+import org.springframework.data.domain.Limit;
 import org.springframework.data.mongodb.core.aggregation.VectorSearchOperation.SearchType;
 import org.springframework.data.mongodb.core.mapping.Field;
 import org.springframework.data.mongodb.core.query.Criteria;
@@ -103,6 +103,16 @@ void mapsCriteriaToDomainType() {
 				.containsExactly(new Document("$vectorSearch", new Document($VECTOR_SEARCH).append("filter", filter)));
 	}
 
+	@Test
+	void withInvalidLimit() {
+
+		VectorSearchOperation $search = VectorSearchOperation.search("vector_index").path("plot_embedding")
+				.vector(-0.0016261312, -0.028070757, -0.011342932).limit(Limit.unlimited());
+
+		List<Document> stages = $search.toPipelineStages(TestAggregationContext.contextFor(Movie.class));
+		assertThat(stages.get(0)).doesNotContainKey("$vectorSearch.limit");
+	}
+
 	static class Movie {
 
 		@Id String id;
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java
index b53531f301..71c395e822 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java
@@ -34,6 +34,7 @@
 import org.bson.Document;
 import org.bson.conversions.Bson;
 import org.bson.types.ObjectId;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.condition.DisabledForJreRange;
@@ -46,7 +47,7 @@
 import org.springframework.data.annotation.AccessType;
 import org.springframework.data.annotation.AccessType.Type;
 import org.springframework.data.annotation.Id;
-import org.springframework.data.annotation.PersistenceConstructor;
+import org.springframework.data.annotation.PersistenceCreator;
 import org.springframework.data.mapping.PersistentPropertyAccessor;
 import org.springframework.data.mapping.PropertyPath;
 import org.springframework.data.mongodb.MongoDatabaseFactory;
@@ -55,7 +56,6 @@
 import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
-import org.springframework.lang.Nullable;
 import org.springframework.test.util.ReflectionTestUtils;
 import org.springframework.util.SerializationUtils;
 
@@ -774,7 +774,7 @@ static class LazyDbRefTargetWithPeristenceConstructor extends LazyDbRefTarget {
 
 		public LazyDbRefTargetWithPeristenceConstructor() {}
 
-		@PersistenceConstructor
+		@PersistenceCreator
 		LazyDbRefTargetWithPeristenceConstructor(String id, String value) {
 			super(id, value);
 			this.persistenceConstructorCalled = true;
@@ -790,7 +790,7 @@ static class LazyDbRefTargetWithPeristenceConstructorWithoutDefaultConstructor e
 
 		boolean persistenceConstructorCalled;
 
-		@PersistenceConstructor
+		@PersistenceCreator
 		LazyDbRefTargetWithPeristenceConstructorWithoutDefaultConstructor(String id, String value) {
 			super(id, value);
 			this.persistenceConstructorCalled = true;
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/GeoConvertersUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/GeoConvertersUnitTests.java
index 7fb664b00c..84a494f9d8 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/GeoConvertersUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/GeoConvertersUnitTests.java
@@ -69,7 +69,7 @@ public void convertsCircleToDocumentAndBackCorrectlyNeutralDistance() {
 	@Test // DATAMONGO-858
 	public void convertsCircleToDocumentAndBackCorrectlyMilesDistance() {
 
-		Distance radius = new Distance(3, Metrics.MILES);
+		Distance radius = Distance.of(3, Metrics.MILES);
 		Circle circle = new Circle(new Point(1, 2), radius);
 
 		Document document = CircleToDocumentConverter.INSTANCE.convert(circle);
@@ -106,7 +106,7 @@ public void convertsSphereToDocumentAndBackCorrectlyWithNeutralDistance() {
 	@Test // DATAMONGO-858
 	public void convertsSphereToDocumentAndBackCorrectlyWithKilometerDistance() {
 
-		Distance radius = new Distance(3, Metrics.KILOMETERS);
+		Distance radius = Distance.of(3, Metrics.KILOMETERS);
 		Sphere sphere = new Sphere(new Point(1, 2), radius);
 
 		Document document = SphereToDocumentConverter.INSTANCE.convert(sphere);
@@ -160,7 +160,7 @@ public void convertsCircleCorrectlyWhenUsingNonDoubleForCoordinates() {
 		circle.put("radius", 3L);
 
 		assertThat(DocumentToCircleConverter.INSTANCE.convert(circle))
-				.isEqualTo(new Circle(new Point(1, 2), new Distance(3)));
+				.isEqualTo(new Circle(new Point(1, 2), Distance.of(3)));
 	}
 
 	@Test // DATAMONGO-1607
@@ -171,7 +171,7 @@ public void convertsSphereCorrectlyWhenUsingNonDoubleForCoordinates() {
 		sphere.put("radius", 3L);
 
 		assertThat(DocumentToSphereConverter.INSTANCE.convert(sphere))
-				.isEqualTo(new Sphere(new Point(1, 2), new Distance(3)));
+				.isEqualTo(new Sphere(new Point(1, 2), Distance.of(3)));
 	}
 
 }
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java
index cf6d69c6c3..6f1c7439c0 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java
@@ -40,6 +40,8 @@
 import org.bson.types.Code;
 import org.bson.types.Decimal128;
 import org.bson.types.ObjectId;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
@@ -63,7 +65,7 @@
 import org.springframework.core.convert.ConverterNotFoundException;
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.data.annotation.Id;
-import org.springframework.data.annotation.PersistenceConstructor;
+import org.springframework.data.annotation.PersistenceCreator;
 import org.springframework.data.annotation.Transient;
 import org.springframework.data.annotation.TypeAlias;
 import org.springframework.data.convert.ConverterBuilder;
@@ -103,10 +105,7 @@
 import org.springframework.data.mongodb.core.mapping.event.AfterConvertCallback;
 import org.springframework.data.projection.EntityProjection;
 import org.springframework.data.projection.EntityProjectionIntrospector;
-import org.springframework.data.util.ClassTypeInformation;
 import org.springframework.data.util.TypeInformation;
-import org.springframework.lang.NonNull;
-import org.springframework.lang.Nullable;
 import org.springframework.test.util.ReflectionTestUtils;
 
 import com.mongodb.BasicDBList;
@@ -1061,7 +1060,7 @@ void convertsSetToBasicDBList() {
 		address.city = "London";
 		address.street = "Foo";
 
-		Object result = converter.convertToMongoType(Collections.singleton(address), ClassTypeInformation.OBJECT);
+		Object result = converter.convertToMongoType(Collections.singleton(address), TypeInformation.OBJECT);
 		assertThat(result).isInstanceOf(List.class);
 
 		Set<?> readResult = converter.read(Set.class, (org.bson.Document) result);
@@ -1393,7 +1392,7 @@ void convertsListToBasicDBListAndRetainsTypeInformationForComplexObjects() {
 		address.street = "Foo";
 
 		Object result = converter.convertToMongoType(Collections.singletonList(address),
-				ClassTypeInformation.from(InterfaceType.class));
+				TypeInformation.of(InterfaceType.class));
 
 		assertThat(result).isInstanceOf(List.class);
 
@@ -1421,7 +1420,7 @@ void convertsArrayToBasicDBListAndRetainsTypeInformationForComplexObjects() {
 		address.city = "London";
 		address.street = "Foo";
 
-		Object result = converter.convertToMongoType(new Address[] { address }, ClassTypeInformation.OBJECT);
+		Object result = converter.convertToMongoType(new Address[] { address }, TypeInformation.OBJECT);
 
 		assertThat(result).isInstanceOf(List.class);
 
@@ -1627,7 +1626,7 @@ void shouldWriteEntityWithGeoSphereCorrectly() {
 	void shouldWriteEntityWithGeoSphereWithMetricDistanceCorrectly() {
 
 		ClassWithGeoSphere object = new ClassWithGeoSphere();
-		Sphere sphere = new Sphere(new Point(1, 2), new Distance(3, Metrics.KILOMETERS));
+		Sphere sphere = new Sphere(new Point(1, 2), Distance.of(3, Metrics.KILOMETERS));
 		Distance radius = sphere.getRadius();
 		object.sphere = sphere;
 
@@ -1712,7 +1711,7 @@ void shouldIncludeTextScorePropertyWhenReading() {
 	}
 
 	@Test // DATAMONGO-1001, DATAMONGO-1509
-	void shouldWriteCglibProxiedClassTypeInformationCorrectly() {
+	void shouldWriteCglibProxiedTypeInformationCorrectly() {
 
 		ProxyFactory factory = new ProxyFactory();
 		factory.setTargetClass(GenericType.class);
@@ -2318,7 +2317,7 @@ void readAndConvertDBRefNestedByMapCorrectly() {
 		Mockito.doReturn(cluster).when(spyConverter).readRef(dbRef);
 
 		Map<Object, Object> result = spyConverter.readMap(spyConverter.getConversionContext(ObjectPath.ROOT), data,
-				ClassTypeInformation.MAP);
+				TypeInformation.MAP);
 
 		assertThat(((Map) result.get("cluster")).get("_id")).isEqualTo(100L);
 	}
@@ -3168,15 +3167,14 @@ void beanConverter() {
 				registrar.registerConverter(WithValueConverters.class, "viaRegisteredConverter",
 						new PropertyValueConverter<String, org.bson.Document, MongoConversionContext>() {
 
-							@Nullable
 							@Override
-							public String read(@Nullable org.bson.Document nativeValue, MongoConversionContext context) {
+							public @Nullable String read(org.bson.@Nullable Document nativeValue, MongoConversionContext context) {
 								return nativeValue.getString("bar");
 							}
 
-							@Nullable
+
 							@Override
-							public org.bson.Document write(@Nullable String domainValue, MongoConversionContext context) {
+							public org.bson.@Nullable Document write(@Nullable String domainValue, MongoConversionContext context) {
 								return new org.bson.Document("bar", domainValue);
 							}
 						});
@@ -3522,7 +3520,7 @@ static class Person implements Contact {
 
 		}
 
-		@PersistenceConstructor
+		@PersistenceCreator
 		public Person(Set<Address> addresses) {
 			this.addresses = addresses;
 		}
@@ -3802,7 +3800,7 @@ static class PrimitiveContainer {
 
 		@Field("property") private int m_property;
 
-		@PersistenceConstructor
+		@PersistenceCreator
 		public PrimitiveContainer(@Value("#root.property") int a_property) {
 			m_property = a_property;
 		}
@@ -3817,7 +3815,7 @@ static class ObjectContainer {
 
 		@Field("property") private PrimitiveContainer m_property;
 
-		@PersistenceConstructor
+		@PersistenceCreator
 		public ObjectContainer(@Value("#root.property") PrimitiveContainer a_property) {
 			m_property = a_property;
 		}
@@ -4084,8 +4082,7 @@ static class WithExplicitTargetTypes {
 		@Field(targetType = FieldType.DECIMAL128) //
 		BigDecimal bigDecimal;
 
-		@Field(targetType = FieldType.DECIMAL128)
-		BigInteger bigInteger;
+		@Field(targetType = FieldType.DECIMAL128) BigInteger bigInteger;
 
 		@Field(targetType = FieldType.INT64) //
 		Date dateAsLong;
@@ -4215,9 +4212,9 @@ public SubTypeOfGenericType convert(org.bson.Document source) {
 	@WritingConverter
 	static class TypeImplementingMapToDocumentConverter implements Converter<TypeImplementingMap, org.bson.Document> {
 
-		@Nullable
+
 		@Override
-		public org.bson.Document convert(TypeImplementingMap source) {
+		public org.bson.@Nullable Document convert(TypeImplementingMap source) {
 			return new org.bson.Document("1st", source.val1).append("2nd", source.val2);
 		}
 	}
@@ -4225,9 +4222,8 @@ public org.bson.Document convert(TypeImplementingMap source) {
 	@ReadingConverter
 	static class DocumentToTypeImplementingMapConverter implements Converter<org.bson.Document, TypeImplementingMap> {
 
-		@Nullable
 		@Override
-		public TypeImplementingMap convert(org.bson.Document source) {
+		public @Nullable TypeImplementingMap convert(org.bson.Document source) {
 			return new TypeImplementingMap(source.getString("1st"), source.getInteger("2nd"));
 		}
 	}
@@ -4413,30 +4409,28 @@ enum Converter2 implements MongoValueConverter<String, org.bson.Document> {
 
 		INSTANCE;
 
-		@Nullable
 		@Override
-		public String read(@Nullable org.bson.Document value, MongoConversionContext context) {
+		public @Nullable String read(org.bson.@Nullable Document value, MongoConversionContext context) {
 			return value.getString("bar");
 		}
 
-		@Nullable
+
 		@Override
-		public org.bson.Document write(@Nullable String value, MongoConversionContext context) {
+		public org.bson.@Nullable Document write(@Nullable String value, MongoConversionContext context) {
 			return new org.bson.Document("bar", value);
 		}
 	}
 
 	static class Converter1 implements MongoValueConverter<String, org.bson.Document> {
 
-		@Nullable
 		@Override
-		public String read(@Nullable org.bson.Document value, MongoConversionContext context) {
+		public @Nullable String read(org.bson.@Nullable Document value, MongoConversionContext context) {
 			return value.getString("foo");
 		}
 
-		@Nullable
+
 		@Override
-		public org.bson.Document write(@Nullable String value, MongoConversionContext context) {
+		public org.bson.@Nullable Document write(@Nullable String value, MongoConversionContext context) {
 			return new org.bson.Document("foo", value);
 		}
 	}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ObjectPathUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ObjectPathUnitTests.java
index b772772444..9e58693faa 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ObjectPathUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ObjectPathUnitTests.java
@@ -23,7 +23,7 @@
 import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity;
 import org.springframework.data.mongodb.core.mapping.Document;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
-import org.springframework.data.util.ClassTypeInformation;
+import org.springframework.data.util.TypeInformation;
 
 /**
  * Unit tests for {@link ObjectPath}.
@@ -39,9 +39,9 @@ public class ObjectPathUnitTests {
 	@BeforeEach
 	public void setUp() {
 
-		one = new BasicMongoPersistentEntity<>(ClassTypeInformation.from(EntityOne.class));
-		two = new BasicMongoPersistentEntity<>(ClassTypeInformation.from(EntityTwo.class));
-		three = new BasicMongoPersistentEntity<>(ClassTypeInformation.from(EntityThree.class));
+		one = new BasicMongoPersistentEntity<>(TypeInformation.of(EntityOne.class));
+		two = new BasicMongoPersistentEntity<>(TypeInformation.of(EntityTwo.class));
+		three = new BasicMongoPersistentEntity<>(TypeInformation.of(EntityThree.class));
 	}
 
 	@Test // DATAMONGO-1703
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ReversingValueConverter.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ReversingValueConverter.java
index eb3b1aba1a..0fe791784d 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ReversingValueConverter.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ReversingValueConverter.java
@@ -15,7 +15,7 @@
  */
 package org.springframework.data.mongodb.core.convert;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /**
  * @author Christoph Strobl
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java
index d8e36c8f67..c646af5539 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java
@@ -544,7 +544,7 @@ void doesNotConvertRawDocuments() {
 	}
 
 	@Test // DATAMONG0-471
-	void testUpdateShouldRetainClassTypeInformationWhenUsing$addToSetWith$eachForCustomTypes() {
+	void testUpdateShouldRetainTypeInformationWhenUsing$addToSetWith$eachForCustomTypes() {
 
 		Update update = new Update().addToSet("models").each(new ModelImpl(2014), new ModelImpl(1), new ModelImpl(28));
 		Document mappedObject = mapper.getMappedObject(update.getUpdateObject(),
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoQueryableEncryptionCollectionCreationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoQueryableEncryptionCollectionCreationTests.java
new file mode 100644
index 0000000000..dd9e459e78
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoQueryableEncryptionCollectionCreationTests.java
@@ -0,0 +1,142 @@
+/*
+ * 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.mongodb.core.encryption;
+
+import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.*;
+import static org.springframework.data.mongodb.core.schema.QueryCharacteristics.*;
+import static org.springframework.data.mongodb.test.util.Assertions.*;
+
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Stream;
+
+import org.bson.BsonBinary;
+import org.bson.Document;
+import org.bson.UuidRepresentation;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
+import org.springframework.data.mongodb.core.CollectionOptions;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.mongodb.core.schema.JsonSchemaProperty;
+import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
+import org.springframework.data.mongodb.test.util.Client;
+import org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion;
+import org.springframework.data.mongodb.test.util.MongoClientExtension;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import com.mongodb.client.MongoClient;
+
+/**
+ * Integration tests for creating collections with encrypted fields.
+ *
+ * @author Christoph Strobl
+ */
+@ExtendWith({ MongoClientExtension.class, SpringExtension.class })
+@EnableIfMongoServerVersion(isGreaterThanEqual = "8.0")
+@ContextConfiguration
+public class MongoQueryableEncryptionCollectionCreationTests {
+
+	public static final String COLLECTION_NAME = "enc-collection";
+	static @Client MongoClient mongoClient;
+
+	@Configuration
+	static class Config extends AbstractMongoClientConfiguration {
+
+		@Override
+		public MongoClient mongoClient() {
+			return mongoClient;
+		}
+
+		@Override
+		protected String getDatabaseName() {
+			return "encryption-schema-tests";
+		}
+
+	}
+
+	@Autowired MongoTemplate template;
+
+	@BeforeEach
+	void beforeEach() {
+		template.dropCollection(COLLECTION_NAME);
+	}
+
+	@ParameterizedTest // GH-4185
+	@MethodSource("collectionOptions")
+	public void createsCollectionWithEncryptedFieldsCorrectly(CollectionOptions collectionOptions) {
+
+		template.createCollection(COLLECTION_NAME, collectionOptions);
+
+		Document encryptedFields = readEncryptedFieldsFromDatabase(COLLECTION_NAME);
+		assertThat(encryptedFields).containsKey("fields");
+
+		List<Document> fields = encryptedFields.get("fields", List.of());
+		assertThat(fields.get(0)).containsEntry("path", "encryptedInt") //
+				.containsEntry("bsonType", "int") //
+				.containsEntry("queries", List
+						.of(Document.parse("{'queryType': 'range', 'contention': { '$numberLong' : '1' }, 'min': 5, 'max': 100}")));
+
+		assertThat(fields.get(1)).containsEntry("path", "nested.encryptedLong") //
+				.containsEntry("bsonType", "long") //
+				.containsEntry("queries", List.of(Document.parse(
+						"{'queryType': 'range', 'contention': { '$numberLong' : '0' }, 'min': { '$numberLong' : '-1' }, 'max': { '$numberLong' : '1' }}")));
+	}
+
+	private static Stream<Arguments> collectionOptions() {
+
+		BsonBinary key1 = new BsonBinary(UUID.randomUUID(), UuidRepresentation.STANDARD);
+		BsonBinary key2 = new BsonBinary(UUID.randomUUID(), UuidRepresentation.STANDARD);
+
+		CollectionOptions manualOptions = CollectionOptions.encryptedCollection(options -> options //
+				.queryable(encrypted(int32("encryptedInt")).keys(key1), range().min(5).max(100).contention(1)) //
+				.queryable(encrypted(JsonSchemaProperty.int64("nested.encryptedLong")).keys(key2),
+						range().min(-1L).max(1L).contention(0)));
+
+		CollectionOptions schemaOptions = CollectionOptions.encryptedCollection(MongoJsonSchema.builder()
+				.property(
+						queryable(encrypted(int32("encryptedInt")).keyId(key1), List.of(range().min(5).max(100).contention(1))))
+				.property(queryable(encrypted(int64("nested.encryptedLong")).keyId(key2),
+						List.of(range().min(-1L).max(1L).contention(0))))
+				.build());
+
+		return Stream.of(Arguments.of(manualOptions), Arguments.of(schemaOptions));
+	}
+
+	Document readEncryptedFieldsFromDatabase(String collectionName) {
+
+		Document collectionInfo = template
+				.executeCommand(new Document("listCollections", 1).append("filter", new Document("name", collectionName)));
+
+		if (collectionInfo.containsKey("cursor")) {
+			collectionInfo = (Document) collectionInfo.get("cursor", Document.class).get("firstBatch", List.class).iterator()
+					.next();
+		}
+
+		if (!collectionInfo.containsKey("options")) {
+			return new Document();
+		}
+
+		return collectionInfo.get("options", Document.class).get("encryptedFields", Document.class);
+	}
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java
new file mode 100644
index 0000000000..e4e760cc91
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java
@@ -0,0 +1,573 @@
+/*
+ * Copyright 2024-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.mongodb.core.encryption;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.springframework.data.mongodb.core.query.Criteria.*;
+
+import java.security.SecureRandom;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+
+import org.assertj.core.api.Assumptions;
+import org.bson.BsonBinary;
+import org.bson.BsonDocument;
+import org.bson.BsonInt32;
+import org.bson.BsonString;
+import org.bson.Document;
+import org.junit.Before;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.convert.PropertyValueConverterFactory;
+import org.springframework.data.convert.ValueConverter;
+import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
+import org.springframework.data.mongodb.core.CollectionOptions;
+import org.springframework.data.mongodb.core.CollectionOptions.EncryptedFieldsOptions;
+import org.springframework.data.mongodb.core.MongoJsonSchemaCreator;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter;
+import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter;
+import org.springframework.data.mongodb.core.mapping.Encrypted;
+import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
+import org.springframework.data.mongodb.core.mapping.Queryable;
+import org.springframework.data.mongodb.core.mapping.RangeEncrypted;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.mongodb.core.query.Update;
+import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
+import org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion;
+import org.springframework.data.mongodb.test.util.EnableIfReplicaSetAvailable;
+import org.springframework.data.mongodb.test.util.MongoClientExtension;
+import org.springframework.data.mongodb.util.MongoClientVersion;
+import org.springframework.data.util.Lazy;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.springframework.util.StringUtils;
+
+import com.mongodb.AutoEncryptionSettings;
+import com.mongodb.ClientEncryptionSettings;
+import com.mongodb.ConnectionString;
+import com.mongodb.MongoClientSettings;
+import com.mongodb.MongoNamespace;
+import com.mongodb.client.MongoClient;
+import com.mongodb.client.MongoClients;
+import com.mongodb.client.MongoCollection;
+import com.mongodb.client.MongoDatabase;
+import com.mongodb.client.model.CreateCollectionOptions;
+import com.mongodb.client.model.CreateEncryptedCollectionParams;
+import com.mongodb.client.model.Filters;
+import com.mongodb.client.model.IndexOptions;
+import com.mongodb.client.model.Indexes;
+import com.mongodb.client.model.vault.EncryptOptions;
+import com.mongodb.client.model.vault.RangeOptions;
+import com.mongodb.client.result.UpdateResult;
+import com.mongodb.client.vault.ClientEncryption;
+import com.mongodb.client.vault.ClientEncryptions;
+
+/**
+ * @author Ross Lawley
+ * @author Christoph Strobl
+ */
+@ExtendWith({ MongoClientExtension.class, SpringExtension.class })
+@EnableIfMongoServerVersion(isGreaterThanEqual = "8.0")
+@EnableIfReplicaSetAvailable
+@ContextConfiguration(classes = RangeEncryptionTests.EncryptionConfig.class)
+class RangeEncryptionTests {
+
+	@Autowired MongoTemplate template;
+	@Autowired MongoClientEncryption clientEncryption;
+	@Autowired EncryptionKeyHolder keyHolder;
+
+	@BeforeEach
+	void clientVersionCheck() {
+		Assumptions.assumeThat(MongoClientVersion.isVersion5orNewer()).isTrue();
+	}
+
+	@AfterEach
+	void tearDown() {
+		template.getDb().getCollection("test").deleteMany(new BsonDocument());
+	}
+
+	@Test // GH-4185
+	void manuallyEncryptedValuesCanBeSavedAndRetrievedCorrectly() {
+
+		EncryptOptions encryptOptions = new EncryptOptions("Range").contentionFactor(1L)
+				.keyId(keyHolder.getEncryptionKey("encryptedInt"))
+				.rangeOptions(new RangeOptions().min(new BsonInt32(0)).max(new BsonInt32(200)).sparsity(1L));
+
+		EncryptOptions encryptExpressionOptions = new EncryptOptions("Range").contentionFactor(1L)
+				.rangeOptions(new RangeOptions().min(new BsonInt32(0)).max(new BsonInt32(200)))
+				.keyId(keyHolder.getEncryptionKey("encryptedInt")).queryType("range");
+
+		EncryptOptions equalityEncOptions = new EncryptOptions("Indexed").contentionFactor(0L)
+				.keyId(keyHolder.getEncryptionKey("age"));
+		;
+
+		EncryptOptions equalityEncOptionsString = new EncryptOptions("Indexed").contentionFactor(0L)
+				.keyId(keyHolder.getEncryptionKey("name"));
+		;
+
+		Document source = new Document("_id", "id-1");
+
+		source.put("name",
+				clientEncryption.getClientEncryption().encrypt(new BsonString("It's a Me, Mario!"), equalityEncOptionsString));
+		source.put("age", clientEncryption.getClientEncryption().encrypt(new BsonInt32(101), equalityEncOptions));
+		source.put("encryptedInt", clientEncryption.getClientEncryption().encrypt(new BsonInt32(101), encryptOptions));
+		source.put("_class", Person.class.getName());
+
+		template.execute(Person.class, col -> col.insertOne(source));
+
+		Document result = template.execute(Person.class, col -> {
+
+			BsonDocument filterSource = new BsonDocument("encryptedInt", new BsonDocument("$gte", new BsonInt32(100)));
+			BsonDocument filter = clientEncryption.getClientEncryption()
+					.encryptExpression(new Document("$and", List.of(filterSource)), encryptExpressionOptions);
+
+			return col.find(filter).first();
+		});
+
+		assertThat(result).containsEntry("encryptedInt", 101);
+	}
+
+	@Test // GH-4185
+	void canLesserThanEqualMatchRangeEncryptedField() {
+
+		Person source = createPerson();
+		template.insert(source);
+
+		Person loaded = template.query(Person.class).matching(where("encryptedInt").lte(source.encryptedInt)).firstValue();
+		assertThat(loaded).isEqualTo(source);
+	}
+
+	@Test // GH-4185
+	void canQueryMixOfEqualityEncryptedAndUnencrypted() {
+
+		Person source = template.insert(createPerson());
+
+		Person loaded = template.query(Person.class)
+				.matching(where("name").is(source.name).and("unencryptedValue").is(source.unencryptedValue)).firstValue();
+		assertThat(loaded).isEqualTo(source);
+	}
+
+	@Test // GH-4185
+	void canQueryMixOfRangeEncryptedAndUnencrypted() {
+
+		Person source = template.insert(createPerson());
+
+		Person loaded = template.query(Person.class)
+				.matching(where("encryptedInt").lte(source.encryptedInt).and("unencryptedValue").is(source.unencryptedValue))
+				.firstValue();
+		assertThat(loaded).isEqualTo(source);
+	}
+
+	@Test // GH-4185
+	void canQueryEqualityEncryptedField() {
+
+		Person source = createPerson();
+		template.insert(source);
+
+		Person loaded = template.query(Person.class).matching(where("age").is(source.age)).firstValue();
+		assertThat(loaded).isEqualTo(source);
+	}
+
+	@Test // GH-4185
+	void canExcludeSafeContentFromResult() {
+
+		Person source = createPerson();
+		template.insert(source);
+
+		Query q = Query.query(where("encryptedLong").lte(1001L).gte(1001L));
+		q.fields().exclude("__safeContent__");
+
+		Person loaded = template.query(Person.class).matching(q).firstValue();
+		assertThat(loaded).isEqualTo(source);
+	}
+
+	@Test // GH-4185
+	void canRangeMatchRangeEncryptedField() {
+
+		Person source = createPerson();
+		template.insert(source);
+
+		Query q = Query.query(where("encryptedLong").lte(1001L).gte(1001L));
+		Person loaded = template.query(Person.class).matching(q).firstValue();
+		assertThat(loaded).isEqualTo(source);
+	}
+
+	@Test // GH-4185
+	void canReplaceEntityWithRangeEncryptedField() {
+
+		Person source = createPerson();
+		template.insert(source);
+
+		source.encryptedInt = 123;
+		source.encryptedLong = 9999L;
+		template.save(source);
+
+		Person loaded = template.query(Person.class).matching(where("id").is(source.id)).firstValue();
+		assertThat(loaded).isEqualTo(source);
+	}
+
+	@Test // GH-4185
+	void canUpdateRangeEncryptedField() {
+
+		Person source = createPerson();
+		template.insert(source);
+
+		UpdateResult updateResult = template.update(Person.class).matching(where("id").is(source.id))
+				.apply(Update.update("encryptedLong", 5000L)).first();
+		assertThat(updateResult.getModifiedCount()).isOne();
+
+		Person loaded = template.query(Person.class).matching(where("id").is(source.id)).firstValue();
+		assertThat(loaded.encryptedLong).isEqualTo(5000L);
+	}
+
+	@Test // GH-4185
+	void errorsWhenUsingNonRangeOperatorEqOnRangeEncryptedField() {
+
+		Person source = createPerson();
+		template.insert(source);
+
+		assertThatThrownBy(
+				() -> template.query(Person.class).matching(where("encryptedInt").is(source.encryptedInt)).firstValue())
+				.isInstanceOf(AssertionError.class)
+				.hasMessageStartingWith("Not a valid range query. Querying a range encrypted field but "
+						+ "the query operator '$eq' for field path 'encryptedInt' is not a range query.");
+	}
+
+	@Test // GH-4185
+	void errorsWhenUsingNonRangeOperatorInOnRangeEncryptedField() {
+
+		Person source = createPerson();
+		template.insert(source);
+
+		assertThatThrownBy(
+				() -> template.query(Person.class).matching(where("encryptedLong").in(1001L, 9999L)).firstValue())
+				.isInstanceOf(AssertionError.class)
+				.hasMessageStartingWith("Not a valid range query. Querying a range encrypted field but "
+						+ "the query operator '$in' for field path 'encryptedLong' is not a range query.");
+	}
+
+	private Person createPerson() {
+
+		Person source = new Person();
+		source.id = "id-1";
+		source.unencryptedValue = "y2k";
+		source.name = "it's a me mario!";
+		source.age = 42;
+		source.encryptedInt = 101;
+		source.encryptedLong = 1001L;
+		source.nested = new NestedWithQEFields();
+		source.nested.value = "Luigi time!";
+		return source;
+	}
+
+	protected static class EncryptionConfig extends AbstractMongoClientConfiguration {
+
+		private static final String LOCAL_KMS_PROVIDER = "local";
+
+		private static final Lazy<Map<String, Map<String, Object>>> LAZY_KMS_PROVIDERS = Lazy.of(() -> {
+			byte[] localMasterKey = new byte[96];
+			new SecureRandom().nextBytes(localMasterKey);
+			return Map.of(LOCAL_KMS_PROVIDER, Map.of("key", localMasterKey));
+		});
+
+		@Autowired ApplicationContext applicationContext;
+
+		@Override
+		protected String getDatabaseName() {
+			return "qe-test";
+		}
+
+		@Bean
+		public MongoClient mongoClient() {
+			return super.mongoClient();
+		}
+
+		@Override
+		protected void configureConverters(MongoConverterConfigurationAdapter converterConfigurationAdapter) {
+			converterConfigurationAdapter
+					.registerPropertyValueConverterFactory(PropertyValueConverterFactory.beanFactoryAware(applicationContext))
+					.useNativeDriverJavaTimeCodecs();
+		}
+
+		@Bean
+		EncryptionKeyHolder keyHolder(MongoClientEncryption mongoClientEncryption) {
+
+			Lazy<Map<String, BsonBinary>> lazyDataKeyMap = Lazy.of(() -> {
+				try (MongoClient client = mongoClient()) {
+
+					MongoDatabase database = client.getDatabase(getDatabaseName());
+					database.getCollection("test").drop();
+
+					ClientEncryption clientEncryption = mongoClientEncryption.getClientEncryption();
+
+					MongoJsonSchema personSchema = MongoJsonSchemaCreator.create(new MongoMappingContext()) // init schema creator
+							.filter(MongoJsonSchemaCreator.encryptedOnly()) //
+							.createSchemaFor(Person.class); //
+
+					Document encryptedFields = CollectionOptions.encryptedCollection(personSchema) //
+							.getEncryptedFieldsOptions() //
+							.map(EncryptedFieldsOptions::toDocument) //
+							.orElseThrow();
+
+					CreateCollectionOptions createCollectionOptions = new CreateCollectionOptions()
+							.encryptedFields(encryptedFields);
+
+					BsonDocument local = clientEncryption.createEncryptedCollection(database, "test", createCollectionOptions,
+							new CreateEncryptedCollectionParams(LOCAL_KMS_PROVIDER));
+
+					Map<String, BsonBinary> keyMap = new LinkedHashMap<>();
+					for (Object o : local.getArray("fields")) {
+						if (o instanceof BsonDocument db) {
+							String path = db.getString("path").getValue();
+							BsonBinary binary = db.getBinary("keyId");
+							for (String part : path.split("\\.")) {
+								keyMap.put(part, binary);
+							}
+						}
+					}
+					return keyMap;
+				}
+			});
+
+			return new EncryptionKeyHolder(lazyDataKeyMap);
+		}
+
+		@Bean
+		MongoEncryptionConverter encryptingConverter(MongoClientEncryption mongoClientEncryption,
+				EncryptionKeyHolder keyHolder) {
+			return new MongoEncryptionConverter(mongoClientEncryption, EncryptionKeyResolver.annotated((ctx) -> {
+
+				String path = ctx.getProperty().getFieldName();
+
+				if (ctx.getProperty().getMongoField().getName().isPath()) {
+					path = StringUtils.arrayToDelimitedString(ctx.getProperty().getMongoField().getName().parts(), ".");
+				}
+				if (ctx.getOperatorContext() != null) {
+					path = ctx.getOperatorContext().path();
+				}
+				return EncryptionKey.keyId(keyHolder.getEncryptionKey(path));
+			}));
+		}
+
+		@Bean
+		CachingMongoClientEncryption clientEncryption(ClientEncryptionSettings encryptionSettings) {
+			return new CachingMongoClientEncryption(() -> ClientEncryptions.create(encryptionSettings));
+		}
+
+		@Override
+		protected void configureClientSettings(MongoClientSettings.Builder builder) {
+			try (MongoClient client = MongoClients.create()) {
+				ClientEncryptionSettings clientEncryptionSettings = encryptionSettings(client);
+
+				builder.autoEncryptionSettings(AutoEncryptionSettings.builder() //
+						.kmsProviders(clientEncryptionSettings.getKmsProviders()) //
+						.keyVaultNamespace(clientEncryptionSettings.getKeyVaultNamespace()) //
+						.bypassQueryAnalysis(true).build());
+			}
+		}
+
+		@Bean
+		ClientEncryptionSettings encryptionSettings(MongoClient mongoClient) {
+			MongoNamespace keyVaultNamespace = new MongoNamespace("encryption.testKeyVault");
+			MongoCollection<Document> keyVaultCollection = mongoClient.getDatabase(keyVaultNamespace.getDatabaseName())
+					.getCollection(keyVaultNamespace.getCollectionName());
+			keyVaultCollection.drop();
+			// Ensure that two data keys cannot share the same keyAltName.
+			keyVaultCollection.createIndex(Indexes.ascending("keyAltNames"),
+					new IndexOptions().unique(true).partialFilterExpression(Filters.exists("keyAltNames")));
+
+			mongoClient.getDatabase(getDatabaseName()).getCollection("test").drop(); // Clear old data
+
+			// Create the ClientEncryption instance
+			return ClientEncryptionSettings.builder() //
+					.keyVaultMongoClientSettings(
+							MongoClientSettings.builder().applyConnectionString(new ConnectionString("mongodb://localhost")).build()) //
+					.keyVaultNamespace(keyVaultNamespace.getFullName()) //
+					.kmsProviders(LAZY_KMS_PROVIDERS.get()) //
+					.build();
+		}
+	}
+
+	static class CachingMongoClientEncryption extends MongoClientEncryption implements DisposableBean {
+
+		static final AtomicReference<ClientEncryption> cache = new AtomicReference<>();
+
+		CachingMongoClientEncryption(Supplier<ClientEncryption> source) {
+			super(() -> {
+				ClientEncryption clientEncryption = cache.get();
+				if (clientEncryption == null) {
+					clientEncryption = source.get();
+					cache.set(clientEncryption);
+				}
+
+				return clientEncryption;
+			});
+		}
+
+		@Override
+		public void destroy() {
+			ClientEncryption clientEncryption = cache.get();
+			if (clientEncryption != null) {
+				clientEncryption.close();
+				cache.set(null);
+			}
+		}
+	}
+
+	static class EncryptionKeyHolder {
+
+		Supplier<Map<String, BsonBinary>> lazyDataKeyMap;
+
+		public EncryptionKeyHolder(Supplier<Map<String, BsonBinary>> lazyDataKeyMap) {
+			this.lazyDataKeyMap = Lazy.of(lazyDataKeyMap);
+		}
+
+		BsonBinary getEncryptionKey(String path) {
+			return lazyDataKeyMap.get().get(path);
+		}
+	}
+
+	@org.springframework.data.mongodb.core.mapping.Document("test")
+	static class Person {
+
+		String id;
+
+		String unencryptedValue;
+
+		@ValueConverter(MongoEncryptionConverter.class)
+		@Encrypted(algorithm = "Indexed") //
+		@Queryable(queryType = "equality", contentionFactor = 0) //
+		String name;
+
+		@ValueConverter(MongoEncryptionConverter.class)
+		@Encrypted(algorithm = "Indexed") //
+		@Queryable(queryType = "equality", contentionFactor = 0) //
+		Integer age;
+
+		@ValueConverter(MongoEncryptionConverter.class)
+		@RangeEncrypted(contentionFactor = 0L,
+				rangeOptions = "{\"min\": 0, \"max\": 200, \"trimFactor\": 1, \"sparsity\": 1}") //
+		Integer encryptedInt;
+
+		@ValueConverter(MongoEncryptionConverter.class)
+		@RangeEncrypted(contentionFactor = 0L,
+				rangeOptions = "{\"min\": {\"$numberLong\": \"1000\"}, \"max\": {\"$numberLong\": \"9999\"}, \"trimFactor\": 1, \"sparsity\": 1}") //
+		Long encryptedLong;
+
+		NestedWithQEFields nested;
+
+		public String getId() {
+			return this.id;
+		}
+
+		public void setId(String id) {
+			this.id = id;
+		}
+
+		public String getName() {
+			return this.name;
+		}
+
+		public void setName(String name) {
+			this.name = name;
+		}
+
+		public Integer getEncryptedInt() {
+			return this.encryptedInt;
+		}
+
+		public void setEncryptedInt(Integer encryptedInt) {
+			this.encryptedInt = encryptedInt;
+		}
+
+		public Long getEncryptedLong() {
+			return this.encryptedLong;
+		}
+
+		public void setEncryptedLong(Long encryptedLong) {
+			this.encryptedLong = encryptedLong;
+		}
+
+		@Override
+		public boolean equals(Object o) {
+			if (o == this) {
+				return true;
+			}
+			if (o == null || getClass() != o.getClass()) {
+				return false;
+			}
+			Person person = (Person) o;
+			return Objects.equals(id, person.id) && Objects.equals(unencryptedValue, person.unencryptedValue)
+					&& Objects.equals(name, person.name) && Objects.equals(age, person.age)
+					&& Objects.equals(encryptedInt, person.encryptedInt) && Objects.equals(encryptedLong, person.encryptedLong);
+		}
+
+		@Override
+		public int hashCode() {
+			return Objects.hash(id, unencryptedValue, name, age, encryptedInt, encryptedLong);
+		}
+
+		@Override
+		public String toString() {
+			return "Person{" + "id='" + id + '\'' + ", unencryptedValue='" + unencryptedValue + '\'' + ", name='" + name
+					+ '\'' + ", age=" + age + ", encryptedInt=" + encryptedInt + ", encryptedLong=" + encryptedLong + '}';
+		}
+	}
+
+	static class NestedWithQEFields {
+
+		@ValueConverter(MongoEncryptionConverter.class)
+		@Encrypted(algorithm = "Indexed") //
+		@Queryable(queryType = "equality", contentionFactor = 0) //
+		String value;
+
+		@Override
+		public String toString() {
+			return "NestedWithQEFields{" + "value='" + value + '\'' + '}';
+		}
+
+		@Override
+		public boolean equals(Object o) {
+			if (o == this) {
+				return true;
+			}
+			if (o == null || getClass() != o.getClass()) {
+				return false;
+			}
+			NestedWithQEFields that = (NestedWithQEFields) o;
+			return Objects.equals(value, that.value);
+		}
+
+		@Override
+		public int hashCode() {
+			return Objects.hash(value);
+		}
+	}
+
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonTests.java
index b81b51abd5..96c685275f 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonTests.java
@@ -33,7 +33,7 @@
 import org.springframework.context.annotation.Configuration;
 import org.springframework.dao.DataAccessException;
 import org.springframework.data.annotation.Id;
-import org.springframework.data.annotation.PersistenceConstructor;
+import org.springframework.data.annotation.PersistenceCreator;
 import org.springframework.data.geo.GeoResults;
 import org.springframework.data.geo.Metrics;
 import org.springframework.data.geo.Point;
@@ -536,7 +536,7 @@ static class Venue2DSphere {
 		private String name;
 		private @GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE) double[] location;
 
-		@PersistenceConstructor
+		@PersistenceCreator
 		public Venue2DSphere(String name, double[] location) {
 			this.name = name;
 			this.location = location;
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatial2DSphereTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatial2DSphereTests.java
index 3a9140d34c..1774c36493 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatial2DSphereTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatial2DSphereTests.java
@@ -23,9 +23,9 @@
 import java.util.List;
 
 import org.junit.Test;
+
 import org.springframework.data.domain.Sort.Direction;
 import org.springframework.data.geo.GeoResults;
-import org.springframework.data.geo.Metric;
 import org.springframework.data.geo.Metrics;
 import org.springframework.data.geo.Point;
 import org.springframework.data.mongodb.core.Venue;
@@ -67,7 +67,7 @@ public void geoNearWithMinDistance() {
 		GeoResults<Venue> result = template.geoNear(geoNear, Venue.class);
 
 		assertThat(result.getContent().size()).isNotEqualTo(0);
-		assertThat(result.getAverageDistance().getMetric()).isEqualTo((Metric) Metrics.KILOMETERS);
+		assertThat(result.getAverageDistance().getMetric()).isEqualTo(Metrics.KILOMETERS);
 	}
 
 	@Test // DATAMONGO-1110
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java
index aa26445f2d..dda16f7849 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java
@@ -31,7 +31,6 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.Suite;
 import org.junit.runners.Suite.SuiteClasses;
-
 import org.springframework.core.annotation.AliasFor;
 import org.springframework.dao.InvalidDataAccessApiUsageException;
 import org.springframework.data.annotation.Id;
@@ -53,7 +52,7 @@
 import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
 import org.springframework.data.mongodb.core.mapping.Unwrapped;
-import org.springframework.data.util.ClassTypeInformation;
+import org.springframework.data.util.TypeInformation;
 
 /**
  * Tests for {@link MongoPersistentEntityIndexResolver}.
@@ -506,7 +505,7 @@ public void resolvesComposedAnnotationIndexDefinitionOptionsCorrectly() {
 			assertThat(indexDefinition.getIndexKeys()).containsEntry("location", "geoHaystack").containsEntry("What light?",
 					1);
 			assertThat(indexDefinition.getIndexOptions()).containsEntry("name", "my_geo_index_name")
-					.containsEntry("bucketSize", 2.0);
+					.doesNotContainKey("bucketSize");
 		}
 
 		@Test // DATAMONGO-2112
@@ -558,9 +557,6 @@ class GeoSpatialIndexedDocumentWithComposedAnnotation {
 			@AliasFor(annotation = GeoSpatialIndexed.class, attribute = "additionalField")
 			String theAdditionalFieldINeedToDefine() default "What light?";
 
-			@AliasFor(annotation = GeoSpatialIndexed.class, attribute = "bucketSize")
-			double size() default 2;
-
 			@AliasFor(annotation = GeoSpatialIndexed.class, attribute = "type")
 			GeoSpatialIndexType indexType() default GeoSpatialIndexType.GEO_HAYSTACK;
 		}
@@ -1186,7 +1182,7 @@ public void shouldNotDetectCycleWhenTypeIsUsedMoreThanOnce() {
 		@SuppressWarnings({ "rawtypes", "unchecked" })
 		public void shouldCatchCyclicReferenceExceptionOnRoot() {
 
-			MongoPersistentEntity entity = new BasicMongoPersistentEntity<>(ClassTypeInformation.from(Object.class));
+			MongoPersistentEntity entity = new BasicMongoPersistentEntity<>(TypeInformation.of(Object.class));
 
 			MongoPersistentProperty propertyMock = mock(MongoPersistentProperty.class);
 			when(propertyMock.isEntity()).thenReturn(true);
@@ -1195,7 +1191,7 @@ public void shouldCatchCyclicReferenceExceptionOnRoot() {
 					new MongoPersistentEntityIndexResolver.CyclicPropertyReferenceException("foo", Object.class, "bar"));
 
 			MongoPersistentEntity<SelfCyclingViaCollectionType> selfCyclingEntity = new BasicMongoPersistentEntity<>(
-					ClassTypeInformation.from(SelfCyclingViaCollectionType.class));
+					TypeInformation.of(SelfCyclingViaCollectionType.class));
 
 			new MongoPersistentEntityIndexResolver(prepareMappingContext(SelfCyclingViaCollectionType.class))
 					.resolveIndexForEntity(selfCyclingEntity);
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/VectorIndexIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/VectorIndexIntegrationTests.java
index dcd447f81a..387f075cb5 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/VectorIndexIntegrationTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/VectorIndexIntegrationTests.java
@@ -22,6 +22,7 @@
 import java.util.List;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -34,7 +35,6 @@
 import org.springframework.data.mongodb.test.util.AtlasContainer;
 import org.springframework.data.mongodb.test.util.MongoTestTemplate;
 import org.springframework.data.mongodb.test.util.MongoTestUtils;
-import org.springframework.lang.Nullable;
 
 import org.testcontainers.junit.jupiter.Container;
 import org.testcontainers.junit.jupiter.Testcontainers;
@@ -200,8 +200,7 @@ void createsVectorIndexWithFilters() throws InterruptedException {
 		});
 	}
 
-	@Nullable
-	private Document readRawIndexInfo(String name) {
+	private @Nullable Document readRawIndexInfo(String name) {
 
 		AggregateIterable<Document> indexes = template.execute(Movie.class, collection -> {
 			return collection.aggregate(List.of(new Document("$listSearchIndexes", new Document("name", name))));
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentPropertyUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentPropertyUnitTests.java
index 116505143e..1037ba4f19 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentPropertyUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentPropertyUnitTests.java
@@ -39,7 +39,7 @@
 import org.springframework.data.mapping.model.Property;
 import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy;
 import org.springframework.data.mapping.model.SimpleTypeHolder;
-import org.springframework.data.util.ClassTypeInformation;
+import org.springframework.data.util.TypeInformation;
 import org.springframework.util.ReflectionUtils;
 
 /**
@@ -56,7 +56,7 @@ public class BasicMongoPersistentPropertyUnitTests {
 
 	@BeforeEach
 	void setup() {
-		entity = new BasicMongoPersistentEntity<>(ClassTypeInformation.from(Person.class));
+		entity = new BasicMongoPersistentEntity<>(TypeInformation.of(Person.class));
 	}
 
 	@Test
@@ -90,7 +90,7 @@ void preventsNegativeOrder() {
 	void usesPropertyAccessForThrowableCause() {
 
 		BasicMongoPersistentEntity<Throwable> entity = new BasicMongoPersistentEntity<>(
-				ClassTypeInformation.from(Throwable.class));
+				TypeInformation.of(Throwable.class));
 		MongoPersistentProperty property = getPropertyFor(entity, "cause");
 
 		assertThat(property.usePropertyAccess()).isTrue();
@@ -99,7 +99,7 @@ void usesPropertyAccessForThrowableCause() {
 	@Test // DATAMONGO-607
 	void usesCustomFieldNamingStrategyByDefault() throws Exception {
 
-		ClassTypeInformation<Person> type = ClassTypeInformation.from(Person.class);
+		TypeInformation<Person> type = TypeInformation.of(Person.class);
 		Field field = ReflectionUtils.findField(Person.class, "lastname");
 
 		MongoPersistentProperty property = new BasicMongoPersistentProperty(Property.of(type, field), entity,
@@ -116,7 +116,7 @@ void usesCustomFieldNamingStrategyByDefault() throws Exception {
 	@Test // DATAMONGO-607
 	void rejectsInvalidValueReturnedByFieldNamingStrategy() {
 
-		ClassTypeInformation<Person> type = ClassTypeInformation.from(Person.class);
+		TypeInformation<Person> type = TypeInformation.of(Person.class);
 		Field field = ReflectionUtils.findField(Person.class, "lastname");
 
 		MongoPersistentProperty property = new BasicMongoPersistentProperty(Property.of(type, field), entity,
@@ -255,7 +255,7 @@ private MongoPersistentProperty getPropertyFor(Field field) {
 	}
 
 	private static <T> MongoPersistentProperty getPropertyFor(Class<T> type, String fieldname) {
-		return getPropertyFor(new BasicMongoPersistentEntity<>(ClassTypeInformation.from(type)), fieldname);
+		return getPropertyFor(new BasicMongoPersistentEntity<>(TypeInformation.of(type)), fieldname);
 	}
 
 	private static MongoPersistentProperty getPropertyFor(MongoPersistentEntity<?> entity, String fieldname) {
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/Person.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/Person.java
index 06f0db6c35..eaed01fc3b 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/Person.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/Person.java
@@ -18,7 +18,7 @@
 import java.util.List;
 
 import org.springframework.data.annotation.Id;
-import org.springframework.data.annotation.PersistenceConstructor;
+import org.springframework.data.annotation.PersistenceCreator;
 import org.springframework.data.annotation.Transient;
 import org.springframework.data.mongodb.core.index.CompoundIndex;
 import org.springframework.data.mongodb.core.index.CompoundIndexes;
@@ -44,7 +44,7 @@ public Person(Integer ssn) {
 		this.ssn = ssn;
 	}
 
-	@PersistenceConstructor
+	@PersistenceCreator
 	public Person(Integer ssn, String firstName, String lastName, Integer age, T address) {
 		this.ssn = ssn;
 		this.firstName = firstName;
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/PersonCustomIdName.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/PersonCustomIdName.java
index a68fe0d531..4a4f7fb126 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/PersonCustomIdName.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/PersonCustomIdName.java
@@ -16,7 +16,7 @@
 package org.springframework.data.mongodb.core.mapping;
 
 import org.springframework.data.annotation.Id;
-import org.springframework.data.annotation.PersistenceConstructor;
+import org.springframework.data.annotation.PersistenceCreator;
 
 /**
  * @author Jon Brisbin <jbrisbin@vmware.com>
@@ -30,7 +30,7 @@ public PersonCustomIdName(Integer ssn, String firstName) {
 		this.firstName = firstName;
 	}
 
-	@PersistenceConstructor
+	@PersistenceCreator
 	public PersonCustomIdName(Integer ssn, String firstName, String lastName) {
 		this.ssn = ssn;
 		this.firstName = firstName;
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ApplicationContextEventTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ApplicationContextEventTests.java
index 9bc1dc78aa..1d44bff5ad 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ApplicationContextEventTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ApplicationContextEventTests.java
@@ -408,7 +408,8 @@ public void publishesEventsForQuerydslFindQueries() {
 		template.save(new Person("Boba", "Fett", 40));
 
 		MongoRepositoryFactory factory = new MongoRepositoryFactory(template);
-		MongoEntityInformation<Person, String> entityInformation = factory.getEntityInformation(Person.class);
+		MongoEntityInformation<Person, String> entityInformation = factory
+				.getEntityInformation(Person.class);
 		QuerydslMongoPredicateExecutor executor = new QuerydslMongoPredicateExecutor<>(entityInformation, template);
 
 		executor.findOne(QPerson.person.lastname.startsWith("Fe"));
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/IndexUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/IndexUnitTests.java
index 156b5b23c6..2ec31e49bc 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/IndexUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/IndexUnitTests.java
@@ -80,9 +80,9 @@ public void testGeospatialIndex2DSphere() {
 	public void testGeospatialIndexGeoHaystack() {
 
 		GeospatialIndex i = new GeospatialIndex("location").typed(GeoSpatialIndexType.GEO_HAYSTACK)
-				.withAdditionalField("name").withBucketSize(40);
+				.withAdditionalField("name");
 		assertThat(i.getIndexKeys()).isEqualTo(Document.parse("{ \"location\" : \"geoHaystack\" , \"name\" : 1}"));
-		assertThat(i.getIndexOptions()).isEqualTo(Document.parse("{ \"bucketSize\" : 40.0}"));
+		assertThat(i.getIndexOptions()).isEqualTo(Document.parse("{ }"));
 	}
 
 	@Test
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/MetricConversionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/MetricConversionUnitTests.java
index bbdad047f2..fdfa840d58 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/MetricConversionUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/MetricConversionUnitTests.java
@@ -17,6 +17,7 @@
 package org.springframework.data.mongodb.core.query;
 
 import static org.assertj.core.api.Assertions.*;
+import static org.assertj.core.data.Offset.*;
 import static org.assertj.core.data.Offset.offset;
 
 import org.junit.jupiter.api.Test;
@@ -34,7 +35,7 @@ public class MetricConversionUnitTests {
 	@Test // DATAMONGO-1348
 	public void shouldConvertMilesToMeters() {
 
-		Distance distance = new Distance(1, Metrics.MILES);
+		Distance distance = Distance.of(1, Metrics.MILES);
 		double distanceInMeters = MetricConversion.getDistanceInMeters(distance);
 
 		assertThat(distanceInMeters).isCloseTo(1609.3438343d, offset(0.000000001));
@@ -43,7 +44,7 @@ public void shouldConvertMilesToMeters() {
 	@Test // DATAMONGO-1348
 	public void shouldConvertKilometersToMeters() {
 
-		Distance distance = new Distance(1, Metrics.KILOMETERS);
+		Distance distance = Distance.of(1, Metrics.KILOMETERS);
 		double distanceInMeters = MetricConversion.getDistanceInMeters(distance);
 
 		assertThat(distanceInMeters).isCloseTo(1000, offset(0.000000001));
@@ -72,11 +73,13 @@ public void shouldCalculateMetersToMilesMultiplier() {
 
 	@Test // GH-4004
 	void shouldConvertKilometersToRadians/* on an earth like sphere with r=6378.137km */() {
-		assertThat(MetricConversion.toRadians(new Distance(1, Metrics.KILOMETERS))).isCloseTo(0.000156785594d, offset(0.000000001));
+		assertThat(MetricConversion.toRadians(Distance.of(1, Metrics.KILOMETERS))).isCloseTo(0.000156785594d,
+				offset(0.000000001));
 	}
 
 	@Test // GH-4004
 	void shouldConvertMilesToRadians/* on an earth like sphere with r=6378.137km */() {
-		assertThat(MetricConversion.toRadians(new Distance(1, Metrics.MILES))).isCloseTo(0.000252321328d, offset(0.000000001));
+		assertThat(MetricConversion.toRadians(Distance.of(1, Metrics.MILES))).isCloseTo(0.000252321328d,
+				offset(0.000000001));
 	}
 }
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/NearQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/NearQueryUnitTests.java
index f4e3d26eb1..2b600988db 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/NearQueryUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/NearQueryUnitTests.java
@@ -21,10 +21,10 @@
 import java.math.RoundingMode;
 
 import org.junit.jupiter.api.Test;
+
 import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.geo.Distance;
-import org.springframework.data.geo.Metric;
 import org.springframework.data.geo.Metrics;
 import org.springframework.data.geo.Point;
 import org.springframework.data.mongodb.core.DocumentTestUtils;
@@ -44,7 +44,7 @@
  */
 public class NearQueryUnitTests {
 
-	private static final Distance ONE_FIFTY_KILOMETERS = new Distance(150, Metrics.KILOMETERS);
+	private static final Distance ONE_FIFTY_KILOMETERS = Distance.of(150, Metrics.KILOMETERS);
 
 	@Test
 	public void rejectsNullPoint() {
@@ -57,7 +57,7 @@ public void settingUpNearWithMetricRecalculatesDistance() {
 		NearQuery query = NearQuery.near(2.5, 2.5, Metrics.KILOMETERS).maxDistance(150);
 
 		assertThat(query.getMaxDistance()).isEqualTo(ONE_FIFTY_KILOMETERS);
-		assertThat(query.getMetric()).isEqualTo((Metric) Metrics.KILOMETERS);
+		assertThat(query.getMetric()).isEqualTo(Metrics.KILOMETERS);
 		assertThat(query.isSpherical()).isTrue();
 	}
 
@@ -68,27 +68,27 @@ public void settingMetricRecalculatesMaxDistance() {
 
 		query.inMiles();
 
-		assertThat(query.getMetric()).isEqualTo((Metric) Metrics.MILES);
+		assertThat(query.getMetric()).isEqualTo(Metrics.MILES);
 	}
 
 	@Test
 	public void configuresResultMetricCorrectly() {
 
 		NearQuery query = NearQuery.near(2.5, 2.1);
-		assertThat(query.getMetric()).isEqualTo((Metric) Metrics.NEUTRAL);
+		assertThat(query.getMetric()).isEqualTo(Metrics.NEUTRAL);
 
 		query = query.maxDistance(ONE_FIFTY_KILOMETERS);
-		assertThat(query.getMetric()).isEqualTo((Metric) Metrics.KILOMETERS);
+		assertThat(query.getMetric()).isEqualTo(Metrics.KILOMETERS);
 		assertThat(query.getMaxDistance()).isEqualTo(ONE_FIFTY_KILOMETERS);
 		assertThat(query.isSpherical()).isTrue();
 
 		query = query.in(Metrics.MILES);
-		assertThat(query.getMetric()).isEqualTo((Metric) Metrics.MILES);
+		assertThat(query.getMetric()).isEqualTo(Metrics.MILES);
 		assertThat(query.getMaxDistance()).isEqualTo(ONE_FIFTY_KILOMETERS);
 		assertThat(query.isSpherical()).isTrue();
 
-		query = query.maxDistance(new Distance(200, Metrics.KILOMETERS));
-		assertThat(query.getMetric()).isEqualTo((Metric) Metrics.MILES);
+		query = query.maxDistance(Distance.of(200, Metrics.KILOMETERS));
+		assertThat(query.getMetric()).isEqualTo(Metrics.MILES);
 	}
 
 	@Test // DATAMONGO-445, DATAMONGO-2264
@@ -200,7 +200,7 @@ public void shouldUseMetersForGeoJsonData() {
 	public void shouldUseMetersForGeoJsonDataWhenDistanceInKilometers() {
 
 		NearQuery query = NearQuery.near(new GeoJsonPoint(27.987901, 86.9165379));
-		query.maxDistance(new Distance(1, Metrics.KILOMETERS));
+		query.maxDistance(Distance.of(1, Metrics.KILOMETERS));
 
 		assertThat(query.toDocument()).containsEntry("maxDistance", 1000D).containsEntry("distanceMultiplier", 0.001D);
 	}
@@ -209,7 +209,7 @@ public void shouldUseMetersForGeoJsonDataWhenDistanceInKilometers() {
 	public void shouldUseMetersForGeoJsonDataWhenDistanceInMiles() {
 
 		NearQuery query = NearQuery.near(new GeoJsonPoint(27.987901, 86.9165379));
-		query.maxDistance(new Distance(1, Metrics.MILES));
+		query.maxDistance(Distance.of(1, Metrics.MILES));
 
 		assertThat(query.toDocument()).containsEntry("maxDistance", 1609.3438343D).containsEntry("distanceMultiplier",
 				0.00062137D);
@@ -219,7 +219,7 @@ public void shouldUseMetersForGeoJsonDataWhenDistanceInMiles() {
 	public void shouldUseKilometersForDistanceWhenMaxDistanceInMiles() {
 
 		NearQuery query = NearQuery.near(new GeoJsonPoint(27.987901, 86.9165379));
-		query.maxDistance(new Distance(1, Metrics.MILES)).in(Metrics.KILOMETERS);
+		query.maxDistance(Distance.of(1, Metrics.MILES)).in(Metrics.KILOMETERS);
 
 		assertThat(query.toDocument()).containsEntry("maxDistance", 1609.3438343D).containsEntry("distanceMultiplier",
 				0.001D);
@@ -229,7 +229,7 @@ public void shouldUseKilometersForDistanceWhenMaxDistanceInMiles() {
 	public void shouldUseMilesForDistanceWhenMaxDistanceInKilometers() {
 
 		NearQuery query = NearQuery.near(new GeoJsonPoint(27.987901, 86.9165379));
-		query.maxDistance(new Distance(1, Metrics.KILOMETERS)).in(Metrics.MILES);
+		query.maxDistance(Distance.of(1, Metrics.KILOMETERS)).in(Metrics.MILES);
 
 		assertThat(query.toDocument()).containsEntry("maxDistance", 1000D).containsEntry("distanceMultiplier", 0.00062137D);
 	}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/TextQueryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/TextQueryTests.java
index 6ea0f5aa9c..b12b83fe3a 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/TextQueryTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/TextQueryTests.java
@@ -21,6 +21,7 @@
 import java.util.List;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -35,7 +36,6 @@
 import org.springframework.data.mongodb.test.util.MongoTemplateExtension;
 import org.springframework.data.mongodb.test.util.MongoTestTemplate;
 import org.springframework.data.mongodb.test.util.Template;
-import org.springframework.lang.Nullable;
 
 /**
  * @author Christoph Strobl
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaUnitTests.java
index 3514927b18..1691305617 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaUnitTests.java
@@ -15,11 +15,14 @@
  */
 package org.springframework.data.mongodb.core.schema;
 
+import static org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty.*;
 import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.*;
+import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.encrypted;
 import static org.springframework.data.mongodb.test.util.Assertions.*;
 
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 import java.util.UUID;
 
 import org.bson.Document;
@@ -105,6 +108,37 @@ void rendersEncryptedPropertyWithKeyIdCorrectly() {
 								.append("algorithm", "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic").append("bsonType", "string"))))));
 	}
 
+	@Test // GH-4185
+	void rendersQueryablePropertyCorrectly() {
+
+		MongoJsonSchema schema = MongoJsonSchema.builder().properties( //
+				queryable(rangeEncrypted(number("ssn")),
+						List.of(QueryCharacteristics.range().contention(0).trimFactor(1).sparsity(1).min(0).max(200))))
+				.build();
+
+		assertThat(schema.toDocument().get("$jsonSchema", Document.class)).isEqualTo("""
+				{
+					"type": "object",
+					"properties": {
+						"ssn": {
+							"encrypt": {
+								"bsonType": "long",
+								"algorithm": "Range",
+								"queries": [{
+									"queryType": "range",
+									"contention": {$numberLong: "0"},
+									"trimFactor": 1,
+									"sparsity": {$numberLong: "1"},
+									"min": 0,
+									"max": 200
+									}]
+							}
+						}
+					}
+				}
+				""");
+	}
+
 	@Test // DATAMONGO-1835
 	void throwsExceptionOnNullRoot() {
 		assertThatIllegalArgumentException().isThrownBy(() -> MongoJsonSchema.of((JsonSchemaObject) null));
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/monitor/MongoMonitorIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/monitor/MongoMonitorIntegrationTests.java
deleted file mode 100644
index e70b398f7f..0000000000
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/monitor/MongoMonitorIntegrationTests.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright 2002-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.mongodb.monitor;
-
-import static org.assertj.core.api.Assertions.*;
-
-import java.net.UnknownHostException;
-
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.springframework.data.mongodb.test.util.Client;
-import org.springframework.data.mongodb.test.util.MongoClientExtension;
-
-import com.mongodb.client.MongoClient;
-
-/**
- * This test class assumes that you are already running the MongoDB server.
- *
- * @author Mark Pollack
- * @author Thomas Darimont
- * @author Mark Paluch
- */
-@ExtendWith(MongoClientExtension.class)
-public class MongoMonitorIntegrationTests {
-
-	static @Client MongoClient mongoClient;
-
-	@Test
-	public void serverInfo() {
-		ServerInfo serverInfo = new ServerInfo(mongoClient);
-		serverInfo.getVersion();
-	}
-
-	@Test // DATAMONGO-685
-	public void getHostNameShouldReturnServerNameReportedByMongo() throws UnknownHostException {
-
-		ServerInfo serverInfo = new ServerInfo(mongoClient);
-
-		String hostName = null;
-		try {
-			hostName = serverInfo.getHostName();
-		} catch (UnknownHostException e) {
-			throw e;
-		}
-
-		assertThat(hostName).isNotNull();
-		assertThat(hostName).isEqualTo("127.0.0.1:27017");
-	}
-
-	@Test
-	public void operationCounters() {
-		OperationCounters operationCounters = new OperationCounters(mongoClient);
-		operationCounters.getInsertCount();
-	}
-}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/PerformanceTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/PerformanceTests.java
index e815cc6e7c..fb8cedd9b1 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/PerformanceTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/PerformanceTests.java
@@ -28,7 +28,7 @@
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.springframework.core.Constants;
-import org.springframework.data.annotation.PersistenceConstructor;
+import org.springframework.data.annotation.PersistenceCreator;
 import org.springframework.data.mongodb.core.MongoTemplate;
 import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory;
 import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
@@ -454,7 +454,7 @@ public Address(String zipCode, String city) {
 			this(zipCode, city, new HashSet<AddressType>(pickRandomNumerOfItemsFrom(Arrays.asList(AddressType.values()))));
 		}
 
-		@PersistenceConstructor
+		@PersistenceCreator
 		public Address(String zipCode, String city, Set<AddressType> types) {
 			this.zipCode = zipCode;
 			this.city = city;
@@ -512,7 +512,7 @@ public Order(List<LineItem> lineItems, Date createdAt) {
 			this.status = Status.ORDERED;
 		}
 
-		@PersistenceConstructor
+		@PersistenceCreator
 		public Order(List<LineItem> lineItems, Date createdAt, Status status) {
 			this.lineItems = lineItems;
 			this.createdAt = createdAt;
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/ReactivePerformanceTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/ReactivePerformanceTests.java
index edda1aad01..a7fba9a046 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/ReactivePerformanceTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/ReactivePerformanceTests.java
@@ -28,11 +28,12 @@
 
 import org.bson.Document;
 import org.bson.types.ObjectId;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.springframework.core.Constants;
-import org.springframework.data.annotation.PersistenceConstructor;
+import org.springframework.data.annotation.PersistenceCreator;
 import org.springframework.data.mongodb.core.ReactiveMongoOperations;
 import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
 import org.springframework.data.mongodb.core.SimpleReactiveMongoDatabaseFactory;
@@ -48,7 +49,6 @@
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
 import org.springframework.data.mongodb.repository.support.ReactiveMongoRepositoryFactory;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.StopWatch;
 import org.springframework.util.StringUtils;
@@ -99,9 +99,8 @@ public void setUp() throws Exception {
 
 		converter = new MappingMongoConverter(new DbRefResolver() {
 
-			@Nullable
 			@Override
-			public Object resolveReference(MongoPersistentProperty property, Object source,
+			public @Nullable Object resolveReference(MongoPersistentProperty property, Object source,
 					ReferenceLookupDelegate referenceLookupDelegate, MongoEntityReader entityReader) {
 				return null;
 			}
@@ -513,7 +512,7 @@ public Address(String zipCode, String city) {
 			this(zipCode, city, new HashSet<AddressType>(pickRandomNumerOfItemsFrom(Arrays.asList(AddressType.values()))));
 		}
 
-		@PersistenceConstructor
+		@PersistenceCreator
 		public Address(String zipCode, String city, Set<AddressType> types) {
 			this.zipCode = zipCode;
 			this.city = city;
@@ -571,7 +570,7 @@ public Order(List<LineItem> lineItems, Date createdAt) {
 			this.status = Status.ORDERED;
 		}
 
-		@PersistenceConstructor
+		@PersistenceCreator
 		public Order(List<LineItem> lineItems, Date createdAt, Status status) {
 			this.lineItems = lineItems;
 			this.createdAt = createdAt;
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java
index 3f2e60f4c4..c2cb6cacf8 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java
@@ -38,6 +38,7 @@
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInstance;
 import org.junit.jupiter.api.extension.ExtendWith;
+
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.dao.DuplicateKeyException;
 import org.springframework.dao.IncorrectResultSizeDataAccessException;
@@ -49,7 +50,6 @@
 import org.springframework.data.geo.Distance;
 import org.springframework.data.geo.GeoPage;
 import org.springframework.data.geo.GeoResults;
-import org.springframework.data.geo.Metric;
 import org.springframework.data.geo.Metrics;
 import org.springframework.data.geo.Point;
 import org.springframework.data.geo.Polygon;
@@ -458,7 +458,7 @@ void executesGeoNearQueryForResultsCorrectly() {
 		repository.save(dave);
 
 		GeoResults<Person> results = repository.findByLocationNear(new Point(-73.99, 40.73),
-				new Distance(2000, Metrics.KILOMETERS));
+				Distance.of(2000, Metrics.KILOMETERS));
 		assertThat(results.getContent()).isNotEmpty();
 	}
 
@@ -470,11 +470,11 @@ void executesGeoPageQueryForResultsCorrectly() {
 		repository.save(dave);
 
 		GeoPage<Person> results = repository.findByLocationNear(new Point(-73.99, 40.73),
-				new Distance(2000, Metrics.KILOMETERS), PageRequest.of(0, 20));
+				Distance.of(2000, Metrics.KILOMETERS), PageRequest.of(0, 20));
 		assertThat(results.getContent()).isNotEmpty();
 
 		// DATAMONGO-607
-		assertThat(results.getAverageDistance().getMetric()).isEqualTo((Metric) Metrics.KILOMETERS);
+		assertThat(results.getAverageDistance().getMetric()).isEqualTo(Metrics.KILOMETERS);
 	}
 
 	@Test // DATAMONGO-323
@@ -634,13 +634,13 @@ void executesGeoPageQueryForWithPageRequestForPageInBetween() {
 		repository.saveAll(Arrays.asList(dave, oliver, carter, boyd, leroi));
 
 		GeoPage<Person> results = repository.findByLocationNear(new Point(-73.99, 40.73),
-				new Distance(2000, Metrics.KILOMETERS), PageRequest.of(1, 2));
+				Distance.of(2000, Metrics.KILOMETERS), PageRequest.of(1, 2));
 
 		assertThat(results.getContent()).isNotEmpty();
 		assertThat(results.getNumberOfElements()).isEqualTo(2);
 		assertThat(results.isFirst()).isFalse();
 		assertThat(results.isLast()).isFalse();
-		assertThat(results.getAverageDistance().getMetric()).isEqualTo((Metric) Metrics.KILOMETERS);
+		assertThat(results.getAverageDistance().getMetric()).isEqualTo(Metrics.KILOMETERS);
 		assertThat(results.getAverageDistance().getNormalizedValue()).isEqualTo(0.0);
 	}
 
@@ -656,12 +656,12 @@ void executesGeoPageQueryForWithPageRequestForPageAtTheEnd() {
 		repository.saveAll(Arrays.asList(dave, oliver, carter));
 
 		GeoPage<Person> results = repository.findByLocationNear(new Point(-73.99, 40.73),
-				new Distance(2000, Metrics.KILOMETERS), PageRequest.of(1, 2));
+				Distance.of(2000, Metrics.KILOMETERS), PageRequest.of(1, 2));
 		assertThat(results.getContent()).isNotEmpty();
 		assertThat(results.getNumberOfElements()).isEqualTo(1);
 		assertThat(results.isFirst()).isFalse();
 		assertThat(results.isLast()).isTrue();
-		assertThat(results.getAverageDistance().getMetric()).isEqualTo((Metric) Metrics.KILOMETERS);
+		assertThat(results.getAverageDistance().getMetric()).isEqualTo(Metrics.KILOMETERS);
 	}
 
 	@Test // DATAMONGO-445
@@ -672,13 +672,13 @@ void executesGeoPageQueryForWithPageRequestForJustOneElement() {
 		repository.save(dave);
 
 		GeoPage<Person> results = repository.findByLocationNear(new Point(-73.99, 40.73),
-				new Distance(2000, Metrics.KILOMETERS), PageRequest.of(0, 2));
+				Distance.of(2000, Metrics.KILOMETERS), PageRequest.of(0, 2));
 
 		assertThat(results.getContent()).isNotEmpty();
 		assertThat(results.getNumberOfElements()).isEqualTo(1);
 		assertThat(results.isFirst()).isTrue();
 		assertThat(results.isLast()).isTrue();
-		assertThat(results.getAverageDistance().getMetric()).isEqualTo((Metric) Metrics.KILOMETERS);
+		assertThat(results.getAverageDistance().getMetric()).isEqualTo(Metrics.KILOMETERS);
 	}
 
 	@Test // DATAMONGO-445
@@ -688,13 +688,13 @@ void executesGeoPageQueryForWithPageRequestForJustOneElementEmptyPage() {
 		repository.save(dave);
 
 		GeoPage<Person> results = repository.findByLocationNear(new Point(-73.99, 40.73),
-				new Distance(2000, Metrics.KILOMETERS), PageRequest.of(1, 2));
+				Distance.of(2000, Metrics.KILOMETERS), PageRequest.of(1, 2));
 
 		assertThat(results.getContent()).isEmpty();
 		assertThat(results.getNumberOfElements()).isEqualTo(0);
 		assertThat(results.isFirst()).isFalse();
 		assertThat(results.isLast()).isTrue();
-		assertThat(results.getAverageDistance().getMetric()).isEqualTo((Metric) Metrics.KILOMETERS);
+		assertThat(results.getAverageDistance().getMetric()).isEqualTo(Metrics.KILOMETERS);
 	}
 
 	@Test // DATAMONGO-1608
@@ -1117,7 +1117,7 @@ void executesGeoNearQueryForResultsCorrectlyWhenGivenMinAndMaxDistance() {
 		dave.setLocation(point);
 		repository.save(dave);
 
-		Range<Distance> range = Distance.between(new Distance(0.01, KILOMETERS), new Distance(2000, KILOMETERS));
+		Range<Distance> range = Distance.between(Distance.of(0.01, KILOMETERS), Distance.of(2000, KILOMETERS));
 
 		GeoResults<Person> results = repository.findPersonByLocationNear(new Point(-73.99, 40.73), range);
 		assertThat(results.getContent()).isNotEmpty();
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Address.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Address.java
index 534f44c8fb..be5be2d9ba 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Address.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Address.java
@@ -14,8 +14,7 @@
  * limitations under the License.
  */
 package org.springframework.data.mongodb.repository;
-
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.ObjectUtils;
 
 import com.querydsl.core.annotations.QueryEmbeddable;
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoRepositoryTextSearchIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoRepositoryTextSearchIntegrationTests.java
index c41abf4aa1..e0c2caee31 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoRepositoryTextSearchIntegrationTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoRepositoryTextSearchIntegrationTests.java
@@ -20,6 +20,7 @@
 import java.util.Arrays;
 import java.util.List;
 
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -38,7 +39,6 @@
 import org.springframework.data.mongodb.test.util.MongoTemplateExtension;
 import org.springframework.data.mongodb.test.util.MongoTestTemplate;
 import org.springframework.data.mongodb.test.util.Template;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ObjectUtils;
 
 /**
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MyId.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MyId.java
index 3dace8928b..4e589d5892 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MyId.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MyId.java
@@ -17,7 +17,7 @@
 
 import java.io.Serializable;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.util.ObjectUtils;
 
 /**
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java
index 664b5279c8..eeca60bc3e 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java
@@ -21,6 +21,7 @@
 import java.util.Set;
 import java.util.UUID;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.geo.Point;
 import org.springframework.data.mongodb.core.index.GeoSpatialIndexType;
 import org.springframework.data.mongodb.core.index.GeoSpatialIndexed;
@@ -30,7 +31,6 @@
 import org.springframework.data.mongodb.core.mapping.DocumentReference;
 import org.springframework.data.mongodb.core.mapping.Field;
 import org.springframework.data.mongodb.core.mapping.Unwrapped;
-import org.springframework.lang.Nullable;
 
 /**
  * Sample domain class.
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonAggregate.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonAggregate.java
index 16b2157bc8..da22801ba6 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonAggregate.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonAggregate.java
@@ -22,7 +22,7 @@
 import java.util.Set;
 
 import org.springframework.data.annotation.Id;
-import org.springframework.data.annotation.PersistenceConstructor;
+import org.springframework.data.annotation.PersistenceCreator;
 
 /**
  * @author Christoph Strobl
@@ -37,7 +37,7 @@ public PersonAggregate(String lastname, String name) {
 		this(lastname, Collections.singletonList(name));
 	}
 
-	@PersistenceConstructor
+	@PersistenceCreator
 	public PersonAggregate(String lastname, Collection<String> names) {
 
 		this.lastname = lastname;
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java
index c66b554078..1f4f682ebc 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java
@@ -23,6 +23,8 @@
 import java.util.regex.Pattern;
 import java.util.stream.Stream;
 
+import org.jspecify.annotations.Nullable;
+
 import org.springframework.data.domain.Limit;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
@@ -43,7 +45,6 @@
 import org.springframework.data.mongodb.repository.Person.Sex;
 import org.springframework.data.querydsl.QuerydslPredicateExecutor;
 import org.springframework.data.repository.query.Param;
-import org.springframework.lang.Nullable;
 
 /**
  * Sample repository managing {@link Person} entities.
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryTransactionalTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryTransactionalTests.java
index 0af684b9c1..b2b350dc4d 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryTransactionalTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryTransactionalTests.java
@@ -27,6 +27,7 @@
 
 import org.bson.Document;
 import org.bson.types.ObjectId;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -46,7 +47,6 @@
 import org.springframework.data.mongodb.test.util.EnableIfReplicaSetAvailable;
 import org.springframework.data.mongodb.test.util.MongoClientExtension;
 import org.springframework.data.mongodb.test.util.ReplSetClient;
-import org.springframework.lang.Nullable;
 import org.springframework.test.annotation.Rollback;
 import org.springframework.test.context.junit.jupiter.SpringExtension;
 import org.springframework.test.context.transaction.AfterTransaction;
@@ -205,9 +205,8 @@ private AfterTransactionAssertion assertAfterTransaction(Person person) {
 
 		AfterTransactionAssertion assertion = new AfterTransactionAssertion<>(new Persistable<Object>() {
 
-			@Nullable
 			@Override
-			public Object getId() {
+			public @Nullable Object getId() {
 				return person.id;
 			}
 
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java
index e89dec21bd..2a76c0ba6c 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java
@@ -20,6 +20,7 @@
 import static org.springframework.data.domain.Sort.Direction.*;
 import static org.springframework.data.mongodb.core.query.Criteria.*;
 import static org.springframework.data.mongodb.core.query.Query.*;
+import static org.springframework.data.mongodb.test.util.Assertions.*;
 import static org.springframework.data.mongodb.test.util.Assertions.assertThat;
 
 import reactor.core.Disposable;
@@ -40,6 +41,7 @@
 import org.junit.jupiter.api.TestInstance;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.reactivestreams.Publisher;
+
 import org.springframework.beans.factory.BeanFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
@@ -72,7 +74,6 @@
 import org.springframework.data.mongodb.test.util.ReactiveMongoClientClosingTestConfiguration;
 import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor;
 import org.springframework.data.repository.Repository;
-import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
 import org.springframework.test.context.junit.jupiter.SpringExtension;
 
 /**
@@ -111,7 +112,6 @@ ReactiveMongoRepositoryFactory factory(ReactiveMongoOperations template, BeanFac
 			factory.setRepositoryBaseClass(SimpleReactiveMongoRepository.class);
 			factory.setBeanClassLoader(beanFactory.getClass().getClassLoader());
 			factory.setBeanFactory(beanFactory);
-			factory.setEvaluationContextProvider(ReactiveQueryMethodEvaluationContextProvider.DEFAULT);
 
 			return factory;
 		}
@@ -355,7 +355,7 @@ void findsPeopleGeoresultByLocationWithinBox() {
 		repository.save(dave).as(StepVerifier::create).expectNextCount(1).verifyComplete();
 
 		repository.findByLocationNear(new Point(-73.99, 40.73), //
-				new Distance(2000, Metrics.KILOMETERS)).as(StepVerifier::create).consumeNextWith(actual -> {
+				Distance.of(2000, Metrics.KILOMETERS)).as(StepVerifier::create).consumeNextWith(actual -> {
 
 					assertThat(actual.getDistance().getValue()).isCloseTo(1, offset(1d));
 					assertThat(actual.getContent()).isEqualTo(dave);
@@ -374,7 +374,7 @@ void findsPeoplePageableGeoresultByLocationWithinBox() throws InterruptedExcepti
 		Thread.sleep(500);
 
 		repository.findByLocationNear(new Point(-73.99, 40.73), //
-				new Distance(2000, Metrics.KILOMETERS), //
+				Distance.of(2000, Metrics.KILOMETERS), //
 				PageRequest.of(0, 10)).as(StepVerifier::create) //
 				.consumeNextWith(actual -> {
 
@@ -395,7 +395,7 @@ void findsPeopleByLocationWithinBox() throws InterruptedException {
 		Thread.sleep(500);
 
 		repository.findPersonByLocationNear(new Point(-73.99, 40.73), //
-				new Distance(2000, Metrics.KILOMETERS)).as(StepVerifier::create) //
+				Distance.of(2000, Metrics.KILOMETERS)).as(StepVerifier::create) //
 				.expectNext(dave) //
 				.verifyComplete();
 	}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveVectorSearchTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveVectorSearchTests.java
new file mode 100644
index 0000000000..14a4749c8a
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveVectorSearchTests.java
@@ -0,0 +1,224 @@
+/*
+ * 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.mongodb.repository;
+
+import static org.assertj.core.api.Assertions.*;
+
+import reactor.core.publisher.Flux;
+import reactor.test.StepVerifier;
+
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.FilterType;
+import org.springframework.data.domain.Limit;
+import org.springframework.data.domain.Score;
+import org.springframework.data.domain.SearchResult;
+import org.springframework.data.domain.Similarity;
+import org.springframework.data.domain.Vector;
+import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
+import org.springframework.data.mongodb.core.SimpleReactiveMongoDatabaseFactory;
+import org.springframework.data.mongodb.core.TestMongoConfiguration;
+import org.springframework.data.mongodb.core.aggregation.VectorSearchOperation;
+import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
+import org.springframework.data.mongodb.core.index.VectorIndex;
+import org.springframework.data.mongodb.core.index.VectorIndex.SimilarityFunction;
+import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories;
+import org.springframework.data.mongodb.test.util.AtlasContainer;
+import org.springframework.data.mongodb.test.util.MongoTestTemplate;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.mongodb.MongoDBAtlasLocalContainer;
+
+import com.mongodb.client.MongoClient;
+import com.mongodb.client.MongoClients;
+
+/**
+ * Integration tests using reactive Vector Search and Vector Indexes through local MongoDB Atlas.
+ *
+ * @author Mark Paluch
+ */
+@Testcontainers(disabledWithoutDocker = true)
+@SpringJUnitConfig(classes = { ReactiveVectorSearchTests.Config.class })
+public class ReactiveVectorSearchTests {
+
+	Vector VECTOR = Vector.of(0.2001f, 0.32345f, 0.43456f, 0.54567f, 0.65678f);
+
+	private static final MongoDBAtlasLocalContainer atlasLocal = AtlasContainer.bestMatch().withReuse(true);
+	private static final String COLLECTION_NAME = "collection-1";
+
+	static MongoClient client;
+	static MongoTestTemplate template;
+
+	@Autowired ReactiveVectorSearchRepository repository;
+
+	@EnableReactiveMongoRepositories(
+			includeFilters = {
+					@ComponentScan.Filter(value = ReactiveVectorSearchRepository.class, type = FilterType.ASSIGNABLE_TYPE) },
+			considerNestedRepositories = true)
+	static class Config extends TestMongoConfiguration {
+
+		@Override
+		public String getDatabaseName() {
+			return "vector-search-tests";
+		}
+
+		@Override
+		public MongoClient mongoClient() {
+			atlasLocal.start();
+			return MongoClients.create(atlasLocal.getConnectionString());
+		}
+
+		@Bean
+		public com.mongodb.reactivestreams.client.MongoClient reactiveMongoClient() {
+			atlasLocal.start();
+			return com.mongodb.reactivestreams.client.MongoClients.create(atlasLocal.getConnectionString());
+		}
+
+		@Bean
+		ReactiveMongoTemplate reactiveMongoTemplate(MappingMongoConverter mongoConverter) {
+			return new ReactiveMongoTemplate(new SimpleReactiveMongoDatabaseFactory(reactiveMongoClient(), getDatabaseName()),
+					mongoConverter);
+		}
+	}
+
+	@BeforeAll
+	static void beforeAll() throws InterruptedException {
+		atlasLocal.start();
+
+		System.out.println(atlasLocal.getConnectionString());
+		client = MongoClients.create(atlasLocal.getConnectionString());
+		template = new MongoTestTemplate(client, "vector-search-tests");
+
+		template.remove(WithVectorFields.class).all();
+		initDocuments();
+		initIndexes();
+
+		Thread.sleep(500); // just wait a little or the index will be broken
+	}
+
+	@Test
+	void shouldSearchEnnWithAnnotatedFilter() {
+
+		Flux<SearchResult<WithVectorFields>> results = repository.searchAnnotated("de", VECTOR, Score.of(0.4),
+				Limit.of(10));
+
+		results.as(StepVerifier::create).consumeNextWith(actual -> {
+			assertThat(actual.getScore().getValue()).isGreaterThan(0.4);
+			assertThat(actual.getScore()).isInstanceOf(Similarity.class);
+
+		}).expectNextCount(2).verifyComplete();
+	}
+
+	@Test
+	void shouldSearchEnnWithDerivedFilter() {
+
+		Flux<WithVectorFields> results = repository.searchByCountryAndEmbeddingNear("de", VECTOR, Limit.of(10));
+
+		results.as(StepVerifier::create).consumeNextWith(actual -> assertThat(actual).isInstanceOf(WithVectorFields.class))
+				.expectNextCount(2).verifyComplete();
+	}
+
+	static void initDocuments() {
+
+		WithVectorFields w1 = new WithVectorFields("de", "one", Vector.of(0.1001f, 0.22345f, 0.33456f, 0.44567f, 0.55678f));
+		WithVectorFields w2 = new WithVectorFields("de", "two", Vector.of(0.2001f, 0.32345f, 0.43456f, 0.54567f, 0.65678f));
+		WithVectorFields w3 = new WithVectorFields("en", "three",
+				Vector.of(0.9001f, 0.82345f, 0.73456f, 0.64567f, 0.55678f));
+		WithVectorFields w4 = new WithVectorFields("de", "four",
+				Vector.of(0.9001f, 0.92345f, 0.93456f, 0.94567f, 0.95678f));
+
+		template.insertAll(List.of(w1, w2, w3, w4));
+	}
+
+	static void initIndexes() {
+
+		VectorIndex cosIndex = new VectorIndex("cos-index")
+				.addVector("embedding", it -> it.similarity(SimilarityFunction.COSINE).dimensions(5)).addFilter("country");
+
+		template.searchIndexOps(WithVectorFields.class).createIndex(cosIndex);
+
+		VectorIndex euclideanIndex = new VectorIndex("euc-index")
+				.addVector("embedding", it -> it.similarity(SimilarityFunction.EUCLIDEAN).dimensions(5)).addFilter("country");
+
+		VectorIndex inner = new VectorIndex("ip-index")
+				.addVector("embedding", it -> it.similarity(SimilarityFunction.DOT_PRODUCT).dimensions(5)).addFilter("country");
+
+		template.searchIndexOps(WithVectorFields.class).createIndex(cosIndex);
+		template.searchIndexOps(WithVectorFields.class).createIndex(euclideanIndex);
+		template.searchIndexOps(WithVectorFields.class).createIndex(inner);
+		template.awaitIndexCreation(WithVectorFields.class, cosIndex.getName());
+		template.awaitIndexCreation(WithVectorFields.class, euclideanIndex.getName());
+		template.awaitIndexCreation(WithVectorFields.class, inner.getName());
+	}
+
+	interface ReactiveVectorSearchRepository extends CrudRepository<WithVectorFields, String> {
+
+		@VectorSearch(indexName = "cos-index", filter = "{country: ?0}", numCandidates = "#{10+10}",
+				searchType = VectorSearchOperation.SearchType.ANN)
+		Flux<SearchResult<WithVectorFields>> searchAnnotated(String country, Vector vector, Score distance, Limit limit);
+
+		@VectorSearch(indexName = "cos-index")
+		Flux<WithVectorFields> searchByCountryAndEmbeddingNear(String country, Vector vector, Limit limit);
+
+	}
+
+	@org.springframework.data.mongodb.core.mapping.Document(COLLECTION_NAME)
+	static class WithVectorFields {
+
+		String id;
+		String country;
+		String description;
+
+		Vector embedding;
+
+		public WithVectorFields(String country, String description, Vector embedding) {
+			this.country = country;
+			this.description = description;
+			this.embedding = embedding;
+		}
+
+		public String getId() {
+			return id;
+		}
+
+		public String getCountry() {
+			return country;
+		}
+
+		public String getDescription() {
+			return description;
+		}
+
+		public Vector getEmbedding() {
+			return embedding;
+		}
+
+		@Override
+		public String toString() {
+			return "WithVectorFields{" + "id='" + id + '\'' + ", country='" + country + '\'' + ", description='" + description
+					+ '\'' + '}';
+		}
+	}
+
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java
index 44235c54ef..751fc51ded 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java
@@ -26,6 +26,7 @@
 import java.util.Objects;
 import java.util.stream.Stream;
 
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.RepeatedTest;
 import org.junit.jupiter.api.Test;
@@ -48,8 +49,6 @@
 import org.springframework.data.mongodb.repository.support.SimpleReactiveMongoRepository;
 import org.springframework.data.mongodb.test.util.EnableIfReplicaSetAvailable;
 import org.springframework.data.repository.query.FluentQuery;
-import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
-import org.springframework.lang.Nullable;
 import org.springframework.test.context.ContextConfiguration;
 import org.springframework.test.context.junit.jupiter.SpringExtension;
 import org.springframework.transaction.TransactionDefinition;
@@ -96,7 +95,6 @@ void setUp() {
 		factory.setRepositoryBaseClass(SimpleReactiveMongoRepository.class);
 		factory.setBeanClassLoader(classLoader);
 		factory.setBeanFactory(beanFactory);
-		factory.setEvaluationContextProvider(ReactiveQueryMethodEvaluationContextProvider.DEFAULT);
 
 		repository = factory.getRepository(ReactivePersonRepository.class);
 		immutableRepository = factory.getRepository(ReactiveImmutablePersonRepository.class);
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/UserWithComplexId.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/UserWithComplexId.java
index 606cca8647..c3bb9cb724 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/UserWithComplexId.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/UserWithComplexId.java
@@ -15,9 +15,9 @@
  */
 package org.springframework.data.mongodb.repository;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.annotation.Id;
 import org.springframework.data.mongodb.core.mapping.Document;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ObjectUtils;
 
 /**
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VectorSearchTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VectorSearchTests.java
new file mode 100644
index 0000000000..a224481da1
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VectorSearchTests.java
@@ -0,0 +1,285 @@
+/*
+ * 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.mongodb.repository;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.FilterType;
+import org.springframework.data.domain.Limit;
+import org.springframework.data.domain.Range;
+import org.springframework.data.domain.Score;
+import org.springframework.data.domain.SearchResult;
+import org.springframework.data.domain.SearchResults;
+import org.springframework.data.domain.Similarity;
+import org.springframework.data.domain.Vector;
+import org.springframework.data.mongodb.core.TestMongoConfiguration;
+import org.springframework.data.mongodb.core.aggregation.VectorSearchOperation;
+import org.springframework.data.mongodb.core.index.VectorIndex;
+import org.springframework.data.mongodb.core.index.VectorIndex.SimilarityFunction;
+import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
+import org.springframework.data.mongodb.test.util.AtlasContainer;
+import org.springframework.data.mongodb.test.util.MongoTestTemplate;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.mongodb.MongoDBAtlasLocalContainer;
+
+import com.mongodb.client.MongoClient;
+import com.mongodb.client.MongoClients;
+
+/**
+ * Integration tests using Vector Search and Vector Indexes through local MongoDB Atlas.
+ *
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ */
+@Testcontainers(disabledWithoutDocker = true)
+@SpringJUnitConfig(classes = { VectorSearchTests.Config.class })
+public class VectorSearchTests {
+
+	Vector VECTOR = Vector.of(0.2001f, 0.32345f, 0.43456f, 0.54567f, 0.65678f);
+
+	private static final MongoDBAtlasLocalContainer atlasLocal = AtlasContainer.bestMatch().withReuse(true);
+	private static final String COLLECTION_NAME = "collection-1";
+
+	static MongoClient client;
+	static MongoTestTemplate template;
+
+	@Autowired VectorSearchRepository repository;
+
+	@EnableMongoRepositories(
+			includeFilters = {
+					@ComponentScan.Filter(value = VectorSearchRepository.class, type = FilterType.ASSIGNABLE_TYPE) },
+			considerNestedRepositories = true)
+	static class Config extends TestMongoConfiguration {
+
+		@Override
+		public String getDatabaseName() {
+			return "vector-search-tests";
+		}
+
+		@Override
+		public MongoClient mongoClient() {
+			return MongoClients.create(atlasLocal.getConnectionString());
+		}
+	}
+
+	@BeforeAll
+	static void beforeAll() throws InterruptedException {
+
+		atlasLocal.start();
+
+		client = MongoClients.create(atlasLocal.getConnectionString());
+		template = new MongoTestTemplate(client, "vector-search-tests");
+
+		template.remove(WithVectorFields.class).all();
+		initDocuments();
+		initIndexes();
+
+		Thread.sleep(500); // just wait a little or the index will be broken
+	}
+
+	@Test
+	void shouldSearchEnnWithAnnotatedFilter() {
+
+		SearchResults<WithVectorFields> results = repository.searchAnnotated("de", VECTOR,
+				Score.of(0.4), Limit.of(10));
+
+		assertThat(results).extracting(SearchResult::getScore).hasOnlyElementsOfType(Similarity.class);
+		assertThat(results).hasSize(3);
+	}
+
+	@Test
+	void shouldSearchEnnWithDerivedFilter() {
+
+		SearchResults<WithVectorFields> results = repository.searchCosineByCountryAndEmbeddingNear("de", VECTOR,
+				Similarity.of(0.98),
+				Limit.of(10));
+
+		assertThat(results).extracting(SearchResult::getScore).hasOnlyElementsOfType(Similarity.class);
+		assertThat(results).hasSize(2).extracting(SearchResult::getContent).extracting(WithVectorFields::getCountry)
+				.containsOnly("de", "de");
+
+		assertThat(results).extracting(SearchResult::getContent).extracting(WithVectorFields::getDescription)
+				.containsExactlyInAnyOrder("two", "one");
+	}
+
+	@Test
+	void shouldSearchEnnWithDerivedFilterWithoutScore() {
+
+		SearchResults<WithVectorFields> de = repository.searchCosineByCountryAndEmbeddingNear("de", VECTOR,
+				Similarity.of(0.4), Limit.of(10));
+
+		assertThat(de).hasSizeGreaterThanOrEqualTo(2);
+
+		assertThat(repository.searchCosineByCountryAndEmbeddingNear("de", VECTOR, Similarity.of(0.999), Limit.of(10)))
+				.hasSize(1);
+	}
+
+	@Test
+	void shouldSearchAsListEnnWithDerivedFilterWithoutScore() {
+
+		List<WithVectorFields> de = repository.searchAsListByCountryAndEmbeddingNear("de", VECTOR, Limit.of(10));
+
+		assertThat(de).hasOnlyElementsOfType(WithVectorFields.class);
+	}
+
+	@Test
+	void shouldSearchEuclideanWithDerivedFilter() {
+
+		SearchResults<WithVectorFields> results = repository.searchEuclideanByCountryAndEmbeddingNear("de", VECTOR,
+				Limit.of(2));
+
+		assertThat(results).hasSize(2).extracting(SearchResult::getContent).extracting(WithVectorFields::getCountry)
+				.containsOnly("de", "de");
+
+		assertThat(results).extracting(SearchResult::getContent).extracting(WithVectorFields::getDescription)
+				.containsExactlyInAnyOrder("two", "one");
+	}
+
+	@Test
+	void shouldSearchEnnWithDerivedFilterWithin() {
+
+		SearchResults<WithVectorFields> results = repository.searchByCountryAndEmbeddingWithin("de", VECTOR,
+				Similarity.between(0.93, 0.98));
+
+		assertThat(results).hasSize(1);
+		for (SearchResult<WithVectorFields> result : results) {
+			assertThat(result.getScore().getValue()).isBetween(0.93, 0.98);
+		}
+	}
+
+	@Test
+	void shouldSearchEnnWithDerivedAndLimitedFilterWithin() {
+
+		SearchResults<WithVectorFields> results = repository.searchTop1ByCountryAndEmbeddingWithin("de", VECTOR,
+				Similarity.between(0.8, 1));
+
+		assertThat(results).hasSize(1);
+
+		for (SearchResult<WithVectorFields> result : results) {
+			assertThat(result.getScore().getValue()).isBetween(0.8, 1.0);
+		}
+	}
+
+	static void initDocuments() {
+
+		WithVectorFields w1 = new WithVectorFields("de", "one", Vector.of(0.1001f, 0.22345f, 0.33456f, 0.44567f, 0.55678f));
+		WithVectorFields w2 = new WithVectorFields("de", "two", Vector.of(0.2001f, 0.32345f, 0.43456f, 0.54567f, 0.65678f));
+		WithVectorFields w3 = new WithVectorFields("en", "three",
+				Vector.of(0.9001f, 0.82345f, 0.73456f, 0.64567f, 0.55678f));
+		WithVectorFields w4 = new WithVectorFields("de", "four",
+				Vector.of(0.9001f, 0.92345f, 0.93456f, 0.94567f, 0.95678f));
+
+		template.insertAll(List.of(w1, w2, w3, w4));
+	}
+
+	static void initIndexes() {
+
+		VectorIndex cosIndex = new VectorIndex("cos-index")
+				.addVector("embedding", it -> it.similarity(SimilarityFunction.COSINE).dimensions(5)).addFilter("country");
+
+		template.searchIndexOps(WithVectorFields.class).createIndex(cosIndex);
+
+		VectorIndex euclideanIndex = new VectorIndex("euc-index")
+				.addVector("embedding", it -> it.similarity(SimilarityFunction.EUCLIDEAN).dimensions(5)).addFilter("country");
+
+		VectorIndex inner = new VectorIndex("ip-index")
+				.addVector("embedding", it -> it.similarity(SimilarityFunction.DOT_PRODUCT).dimensions(5)).addFilter("country");
+
+		template.searchIndexOps(WithVectorFields.class).createIndex(cosIndex);
+		template.searchIndexOps(WithVectorFields.class).createIndex(euclideanIndex);
+		template.searchIndexOps(WithVectorFields.class).createIndex(inner);
+		template.awaitIndexCreation(WithVectorFields.class, cosIndex.getName());
+		template.awaitIndexCreation(WithVectorFields.class, euclideanIndex.getName());
+		template.awaitIndexCreation(WithVectorFields.class, inner.getName());
+	}
+
+	interface VectorSearchRepository extends CrudRepository<WithVectorFields, String> {
+
+		@VectorSearch(indexName = "cos-index", filter = "{country: ?0}", numCandidates = "#{10+10}",
+				searchType = VectorSearchOperation.SearchType.ANN)
+		SearchResults<WithVectorFields> searchAnnotated(String country, Vector vector,
+				Score distance, Limit limit);
+
+		@VectorSearch(indexName = "cos-index")
+		SearchResults<WithVectorFields> searchCosineByCountryAndEmbeddingNear(String country, Vector vector,
+				Score similarity, Limit limit);
+
+		@VectorSearch(indexName = "cos-index")
+		List<WithVectorFields> searchAsListByCountryAndEmbeddingNear(String country, Vector vector, Limit limit);
+
+		@VectorSearch(indexName = "euc-index")
+		SearchResults<WithVectorFields> searchEuclideanByCountryAndEmbeddingNear(String country, Vector vector,
+				Limit limit);
+
+		@VectorSearch(indexName = "cos-index", limit = "10")
+		SearchResults<WithVectorFields> searchByCountryAndEmbeddingWithin(String country, Vector vector,
+				Range<Similarity> distance);
+
+		@VectorSearch(indexName = "cos-index")
+		SearchResults<WithVectorFields> searchTop1ByCountryAndEmbeddingWithin(String country, Vector vector,
+				Range<Similarity> distance);
+
+	}
+
+	@org.springframework.data.mongodb.core.mapping.Document(COLLECTION_NAME)
+	static class WithVectorFields {
+
+		String id;
+		String country;
+		String description;
+
+		Vector embedding;
+
+		public WithVectorFields(String country, String description, Vector embedding) {
+			this.country = country;
+			this.description = description;
+			this.embedding = embedding;
+		}
+
+		public String getId() {
+			return id;
+		}
+
+		public String getCountry() {
+			return country;
+		}
+
+		public String getDescription() {
+			return description;
+		}
+
+		public Vector getEmbedding() {
+			return embedding;
+		}
+
+		@Override
+		public String toString() {
+			return "WithVectorFields{" + "id='" + id + '\'' + ", country='" + country + '\'' + ", description='" + description
+					+ '\'' + '}';
+		}
+	}
+
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VersionedPerson.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VersionedPerson.java
index 294e4ea501..4f1adc714e 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VersionedPerson.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VersionedPerson.java
@@ -17,9 +17,9 @@
 
 import java.util.Objects;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.annotation.Version;
 import org.springframework.data.mongodb.core.mapping.Document;
-import org.springframework.lang.Nullable;
 
 /**
  * @author Christoph Strobl
@@ -48,8 +48,7 @@ public String getFirstname() {
 		return this.firstname;
 	}
 
-	@Nullable
-	public String getLastname() {
+	public @Nullable String getLastname() {
 		return this.lastname;
 	}
 
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VersionedPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VersionedPersonRepositoryIntegrationTests.java
index f4e1e0282e..917a1094d8 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VersionedPersonRepositoryIntegrationTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VersionedPersonRepositoryIntegrationTests.java
@@ -19,6 +19,7 @@
 
 import org.bson.Document;
 import org.bson.types.ObjectId;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -34,7 +35,6 @@
 import org.springframework.data.mongodb.test.util.MongoClientExtension;
 import org.springframework.data.mongodb.test.util.MongoTestUtils;
 import org.springframework.data.repository.CrudRepository;
-import org.springframework.lang.Nullable;
 import org.springframework.test.context.ContextConfiguration;
 import org.springframework.test.context.junit.jupiter.SpringExtension;
 
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotContributionIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotContributionIntegrationTests.java
new file mode 100644
index 0000000000..b46b1dfb50
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotContributionIntegrationTests.java
@@ -0,0 +1,109 @@
+/*
+ * 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.mongodb.repository.aot;
+
+import static net.javacrumbs.jsonunit.assertj.JsonAssertions.*;
+import static org.mockito.Mockito.*;
+
+import example.aot.User;
+import example.aot.UserRepository;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.aot.generate.GeneratedFiles;
+import org.springframework.aot.test.generate.TestGenerationContext;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.FilterType;
+import org.springframework.context.aot.ApplicationContextAotGenerator;
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.core.io.InputStreamSource;
+import org.springframework.data.aot.AotContext;
+import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
+import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
+import org.springframework.data.querydsl.QuerydslPredicateExecutor;
+import org.springframework.mock.env.MockPropertySource;
+
+import com.mongodb.client.MongoClient;
+
+/**
+ * Integration tests for AOT processing of imperative repositories.
+ *
+ * @author Mark Paluch
+ */
+class AotContributionIntegrationTests {
+
+	@EnableMongoRepositories(considerNestedRepositories = true, includeFilters = {
+			@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = QuerydslUserRepository.class) })
+	static class AotConfiguration extends AbstractMongoClientConfiguration {
+
+		@Override
+		public MongoClient mongoClient() {
+			return mock(MongoClient.class);
+		}
+
+		@Override
+		protected String getDatabaseName() {
+			return "";
+		}
+	}
+
+	interface QuerydslUserRepository extends UserRepository, QuerydslPredicateExecutor<User> {
+
+	}
+
+	@Test // GH-3830
+	void shouldGenerateMetadataForBaseRepositoryAndQuerydslFragment() throws IOException {
+
+		TestGenerationContext generationContext = generate(AotConfiguration.class);
+
+		InputStreamSource metadata = generationContext.getGeneratedFiles().getGeneratedFile(GeneratedFiles.Kind.RESOURCE,
+				QuerydslUserRepository.class.getName().replace('.', '/') + ".json");
+
+		InputStreamResource isr = new InputStreamResource(metadata);
+		String json = isr.getContentAsString(StandardCharsets.UTF_8);
+
+		assertThatJson(json).isObject() //
+				.containsEntry("name", QuerydslUserRepository.class.getName()) //
+				.containsEntry("module", "MongoDB") //
+				.containsEntry("type", "IMPERATIVE");
+
+		assertThatJson(json).inPath("$.methods[?(@.name == 'findBy')].fragment").isArray().first().isObject()
+				.containsEntry("interface", "org.springframework.data.querydsl.QuerydslPredicateExecutor").containsEntry(
+						"fragment", "org.springframework.data.mongodb.repository.support.QuerydslMongoPredicateExecutor");
+
+		assertThatJson(json).inPath("$.methods[?(@.name == 'existsById')].fragment").isArray().first().isObject()
+				.containsEntry("fragment", "org.springframework.data.mongodb.repository.support.SimpleMongoRepository");
+	}
+
+	private static TestGenerationContext generate(Class<?>... configurationClasses) {
+
+		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
+		context.getEnvironment().getPropertySources()
+				.addFirst(new MockPropertySource().withProperty(AotContext.GENERATED_REPOSITORIES_ENABLED, "true"));
+		context.register(configurationClasses);
+
+		ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator();
+
+		TestGenerationContext generationContext = new TestGenerationContext();
+		generator.processAheadOfTime(context, generationContext);
+		return generationContext;
+	}
+
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java
new file mode 100644
index 0000000000..eba08ecc2e
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java
@@ -0,0 +1,130 @@
+/*
+ * 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.mongodb.repository.aot;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+import org.springframework.aot.test.generate.TestGenerationContext;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.beans.factory.support.AbstractBeanDefinition;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.core.test.tools.TestCompiler;
+import org.springframework.data.projection.ProjectionFactory;
+import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
+import org.springframework.data.repository.query.ValueExpressionDelegate;
+import org.springframework.util.ReflectionUtils;
+
+/**
+ * Test Configuration Support Class for generated AOT Repository Fragments based on a Repository Interface.
+ * <p>
+ * This configuration generates the AOT repository, compiles sources and configures a BeanFactory to contain the AOT
+ * fragment. Additionally, the fragment is exposed through a {@code repositoryInterface} JDK proxy forwarding method
+ * invocations to the backing AOT fragment. Note that {@code repositoryInterface} is not a repository proxy.
+ *
+ * @author Christoph Strobl
+ */
+public class AotFragmentTestConfigurationSupport implements BeanFactoryPostProcessor {
+
+	private final Class<?> repositoryInterface;
+	private final TestMongoAotRepositoryContext repositoryContext;
+
+	public AotFragmentTestConfigurationSupport(Class<?> repositoryInterface) {
+
+		this.repositoryInterface = repositoryInterface;
+		this.repositoryContext = new TestMongoAotRepositoryContext(repositoryInterface, null);
+	}
+
+	@Override
+	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
+
+		TestGenerationContext generationContext = new TestGenerationContext(repositoryInterface);
+
+		new MongoRepositoryContributor(repositoryContext).contribute(generationContext);
+
+		AbstractBeanDefinition aotGeneratedRepository = BeanDefinitionBuilder
+				.genericBeanDefinition(
+						repositoryInterface.getPackageName() + "." + repositoryInterface.getSimpleName() + "Impl__Aot") //
+				.addConstructorArgReference("mongoOperations") //
+				.addConstructorArgValue(getCreationContext(repositoryContext)).getBeanDefinition();
+
+		TestCompiler.forSystem().withCompilerOptions("-parameters").with(generationContext).compile(compiled -> {
+			beanFactory.setBeanClassLoader(compiled.getClassLoader());
+			((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("fragment", aotGeneratedRepository);
+		});
+
+		BeanDefinition fragmentFacade = BeanDefinitionBuilder.rootBeanDefinition((Class) repositoryInterface, () -> {
+
+			Object fragment = beanFactory.getBean("fragment");
+			Object proxy = getFragmentFacadeProxy(fragment);
+
+			return repositoryInterface.cast(proxy);
+		}).getBeanDefinition();
+
+		((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("fragmentFacade", fragmentFacade);
+
+		beanFactory.registerSingleton("generationContext", generationContext);
+	}
+
+	private Object getFragmentFacadeProxy(Object fragment) {
+
+		return Proxy.newProxyInstance(repositoryInterface.getClassLoader(), new Class<?>[] { repositoryInterface },
+				(p, method, args) -> {
+
+					Method target = ReflectionUtils.findMethod(fragment.getClass(), method.getName(), method.getParameterTypes());
+
+					if (target == null) {
+						throw new NoSuchMethodException("Method [%s] is not implemented by [%s]".formatted(method, target));
+					}
+
+					try {
+						return target.invoke(fragment, args);
+					} catch (ReflectiveOperationException e) {
+						ReflectionUtils.handleReflectionException(e);
+					}
+
+					return null;
+				});
+	}
+
+	private RepositoryFactoryBeanSupport.FragmentCreationContext getCreationContext(
+			TestMongoAotRepositoryContext repositoryContext) {
+
+		return new RepositoryFactoryBeanSupport.FragmentCreationContext() {
+
+			@Override
+			public RepositoryMetadata getRepositoryMetadata() {
+				return repositoryContext.getRepositoryInformation();
+			}
+
+			@Override
+			public ValueExpressionDelegate getValueExpressionDelegate() {
+				return ValueExpressionDelegate.create();
+			}
+
+			@Override
+			public ProjectionFactory getProjectionFactory() {
+				return new SpelAwareProxyProjectionFactory();
+			}
+		};
+	}
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java
new file mode 100644
index 0000000000..1c9796ead6
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java
@@ -0,0 +1,705 @@
+/*
+ * 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.mongodb.repository.aot;
+
+import static org.assertj.core.api.Assertions.*;
+
+import example.aot.User;
+import example.aot.UserProjection;
+import example.aot.UserRepository;
+import example.aot.UserRepository.UserAggregate;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.Optional;
+
+import org.bson.Document;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.domain.KeysetScrollPosition;
+import org.springframework.data.domain.Limit;
+import org.springframework.data.domain.OffsetScrollPosition;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.ScrollPosition;
+import org.springframework.data.domain.Slice;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Window;
+import org.springframework.data.mongodb.core.MongoOperations;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.mongodb.core.aggregation.AggregationResults;
+import org.springframework.data.mongodb.test.util.Client;
+import org.springframework.data.mongodb.test.util.MongoClientExtension;
+import org.springframework.data.mongodb.test.util.MongoTestUtils;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+import org.springframework.util.StringUtils;
+
+import com.mongodb.client.MongoClient;
+
+/**
+ * Integration tests for the {@link UserRepository} AOT fragment.
+ *
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ */
+@ExtendWith(MongoClientExtension.class)
+@SpringJUnitConfig(classes = MongoRepositoryContributorTests.MongoRepositoryContributorConfiguration.class)
+class MongoRepositoryContributorTests {
+
+	private static final String DB_NAME = "aot-repo-tests";
+
+	@Client static MongoClient client;
+	@Autowired UserRepository fragment;
+
+	@Configuration
+	static class MongoRepositoryContributorConfiguration extends AotFragmentTestConfigurationSupport {
+
+		public MongoRepositoryContributorConfiguration() {
+			super(UserRepository.class);
+		}
+
+		@Bean
+		MongoOperations mongoOperations() {
+			return new MongoTemplate(client, DB_NAME);
+		}
+	}
+
+	@BeforeEach
+	void beforeEach() {
+
+		MongoTestUtils.flushCollection(DB_NAME, "user", client);
+		initUsers();
+	}
+
+	@Test
+	void testFindDerivedFinderSingleEntity() {
+
+		User user = fragment.findOneByUsername("yoda");
+		assertThat(user).isNotNull().extracting(User::getUsername).isEqualTo("yoda");
+	}
+
+	@Test
+	void testFindDerivedFinderOptionalEntity() {
+
+		Optional<User> user = fragment.findOptionalOneByUsername("yoda");
+		assertThat(user).isNotNull().containsInstanceOf(User.class)
+				.hasValueSatisfying(it -> assertThat(it).extracting(User::getUsername).isEqualTo("yoda"));
+	}
+
+	@Test
+	void testDerivedCount() {
+
+		assertThat(fragment.countUsersByLastname("Skywalker")).isEqualTo(2L);
+		assertThat(fragment.countUsersAsIntByLastname("Skywalker")).isEqualTo(2);
+	}
+
+	@Test
+	void testDerivedExists() {
+
+		assertThat(fragment.existsUserByLastname("Skywalker")).isTrue();
+	}
+
+	@Test
+	void testDerivedFinderWithoutArguments() {
+
+		List<User> users = fragment.findUserNoArgumentsBy();
+		assertThat(users).hasSize(7).hasOnlyElementsOfType(User.class);
+	}
+
+	@Test
+	void testCountWorksAsExpected() {
+
+		Long value = fragment.countUsersByLastname("Skywalker");
+		assertThat(value).isEqualTo(2L);
+	}
+
+	@Test
+	void testDerivedFinderReturningList() {
+
+		List<User> users = fragment.findByLastnameStartingWith("S");
+		assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("luke", "vader", "kylo", "han");
+	}
+
+	@Test
+	void testEndingWith() {
+
+		List<User> users = fragment.findByLastnameEndsWith("er");
+		assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("luke", "vader");
+	}
+
+	@Test
+	void testLike() {
+
+		List<User> users = fragment.findByFirstnameLike("ei");
+		assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("leia");
+	}
+
+	@Test
+	void testNotLike() {
+
+		List<User> users = fragment.findByFirstnameNotLike("ei");
+		assertThat(users).extracting(User::getUsername).isNotEmpty().doesNotContain("leia");
+	}
+
+	@Test
+	void testIn() {
+
+		List<User> users = fragment.findByUsernameIn(List.of("chewbacca", "kylo"));
+		assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("chewbacca", "kylo");
+	}
+
+	@Test
+	void testNotIn() {
+
+		List<User> users = fragment.findByUsernameNotIn(List.of("chewbacca", "kylo"));
+		assertThat(users).extracting(User::getUsername).isNotEmpty().doesNotContain("chewbacca", "kylo");
+	}
+
+	@Test
+	void testAnd() {
+
+		List<User> users = fragment.findByFirstnameAndLastname("Han", "Solo");
+		assertThat(users).extracting(User::getUsername).containsExactly("han");
+	}
+
+	@Test
+	void testOr() {
+
+		List<User> users = fragment.findByFirstnameOrLastname("Han", "Skywalker");
+		assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("han", "vader", "luke");
+	}
+
+	@Test
+	void testBetween() {
+
+		List<User> users = fragment.findByVisitsBetween(10, 100);
+		assertThat(users).extracting(User::getUsername).containsExactly("vader");
+	}
+
+	@Test
+	void testTimeValue() {
+
+		List<User> users = fragment.findByLastSeenGreaterThan(Instant.parse("2025-01-01T00:00:00.000Z"));
+		assertThat(users).extracting(User::getUsername).containsExactly("luke");
+	}
+
+	@Test
+	void testNot() {
+
+		List<User> users = fragment.findByLastnameNot("Skywalker");
+		assertThat(users).extracting(User::getUsername).isNotEmpty().doesNotContain("luke", "vader");
+	}
+
+	@Test
+	void testExistsCriteria() {
+
+		List<User> users = fragment.findByVisitsExists(false);
+		assertThat(users).extracting(User::getUsername).contains("kylo");
+	}
+
+	@Test
+	void testLimitedDerivedFinder() {
+
+		List<User> users = fragment.findTop2ByLastnameStartingWith("S");
+		assertThat(users).hasSize(2);
+	}
+
+	@Test
+	void testSortedDerivedFinder() {
+
+		List<User> users = fragment.findByLastnameStartingWithOrderByUsername("S");
+		assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader");
+	}
+
+	@Test
+	void testDerivedFinderWithLimitArgument() {
+
+		List<User> users = fragment.findByLastnameStartingWith("S", Limit.of(2));
+		assertThat(users).hasSize(2);
+	}
+
+	@Test
+	void testDerivedFinderWithSort() {
+
+		List<User> users = fragment.findByLastnameStartingWith("S", Sort.by("username"));
+		assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader");
+	}
+
+	@Test
+	void testDerivedFinderWithSortAndLimit() {
+
+		List<User> users = fragment.findByLastnameStartingWith("S", Sort.by("username"), Limit.of(2));
+		assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo");
+	}
+
+	@Test
+	void testDerivedFinderReturningListWithPageable() {
+
+		List<User> users = fragment.findByLastnameStartingWith("S", PageRequest.of(0, 2, Sort.by("username")));
+		assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo");
+	}
+
+	@Test
+	void testDerivedFinderReturningPage() {
+
+		Page<User> page = fragment.findPageOfUsersByLastnameStartingWith("S", PageRequest.of(0, 2, Sort.by("username")));
+		assertThat(page.getTotalElements()).isEqualTo(4);
+		assertThat(page.getSize()).isEqualTo(2);
+		assertThat(page.getContent()).extracting(User::getUsername).containsExactly("han", "kylo");
+	}
+
+	@Test
+	void testDerivedFinderReturningSlice() {
+
+		Slice<User> slice = fragment.findSliceOfUserByLastnameStartingWith("S", PageRequest.of(0, 2, Sort.by("username")));
+		assertThat(slice.hasNext()).isTrue();
+		assertThat(slice.getSize()).isEqualTo(2);
+		assertThat(slice.getContent()).extracting(User::getUsername).containsExactly("han", "kylo");
+	}
+
+	@Test
+	void testDerivedQueryReturningStream() {
+
+		List<User> results = fragment.streamByLastnameStartingWith("S", Sort.by("username"), Limit.of(2)).toList();
+
+		assertThat(results).hasSize(2);
+		assertThat(results).extracting(User::getUsername).containsExactly("han", "kylo");
+	}
+
+	@Test
+	void testDerivedQueryReturningWindowByOffset() {
+
+		Window<User> window1 = fragment.findTop2WindowByLastnameStartingWithOrderByUsername("S", ScrollPosition.offset());
+		assertThat(window1).extracting(User::getUsername).containsExactly("han", "kylo");
+		assertThat(window1.positionAt(1)).isInstanceOf(OffsetScrollPosition.class);
+
+		Window<User> window2 = fragment.findTop2WindowByLastnameStartingWithOrderByUsername("S", window1.positionAt(1));
+		assertThat(window2).extracting(User::getUsername).containsExactly("luke", "vader");
+	}
+
+	@Test
+	void testDerivedQueryReturningWindowByKeyset() {
+
+		Window<User> window1 = fragment.findTop2WindowByLastnameStartingWithOrderByUsername("S", ScrollPosition.keyset());
+		assertThat(window1).extracting(User::getUsername).containsExactly("han", "kylo");
+		assertThat(window1.positionAt(1)).isInstanceOf(KeysetScrollPosition.class);
+
+		Window<User> window2 = fragment.findTop2WindowByLastnameStartingWithOrderByUsername("S", window1.positionAt(1));
+		assertThat(window2).extracting(User::getUsername).containsExactly("luke", "vader");
+	}
+
+	@Test
+	void testAnnotatedFinderReturningSingleValueWithQuery() {
+
+		User user = fragment.findAnnotatedQueryByUsername("yoda");
+		assertThat(user).isNotNull().extracting(User::getUsername).isEqualTo("yoda");
+	}
+
+	@Test
+	void testAnnotatedCount() {
+
+		Long value = fragment.countAnnotatedQueryByLastname("Skywalker");
+		assertThat(value).isEqualTo(2L);
+	}
+
+	@Test
+	void testAnnotatedFinderReturningListWithQuery() {
+
+		List<User> users = fragment.findAnnotatedQueryByLastname("S");
+		assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader");
+	}
+
+	@Test
+	void testAnnotatedMultilineFinderWithQuery() {
+
+		List<User> users = fragment.findAnnotatedMultilineQueryByLastname("S");
+		assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader");
+	}
+
+	@Test
+	void testAnnotatedFinderWithQueryAndLimit() {
+
+		List<User> users = fragment.findAnnotatedQueryByLastname("S", Limit.of(2));
+		assertThat(users).hasSize(2);
+	}
+
+	@Test
+	void testAnnotatedFinderWithQueryAndSort() {
+
+		List<User> users = fragment.findAnnotatedQueryByLastname("S", Sort.by("username"));
+		assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader");
+	}
+
+	@Test
+	void testAnnotatedFinderWithQueryLimitAndSort() {
+
+		List<User> users = fragment.findAnnotatedQueryByLastname("S", Limit.of(2), Sort.by("username"));
+		assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo");
+	}
+
+	@Test
+	void testAnnotatedFinderReturningListWithPageable() {
+
+		List<User> users = fragment.findAnnotatedQueryByLastname("S", PageRequest.of(0, 2, Sort.by("username")));
+		assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo");
+	}
+
+	@Test
+	void testAnnotatedFinderReturningPage() {
+
+		Page<User> page = fragment.findAnnotatedQueryPageOfUsersByLastname("S", PageRequest.of(0, 2, Sort.by("username")));
+		assertThat(page.getTotalElements()).isEqualTo(4);
+		assertThat(page.getSize()).isEqualTo(2);
+		assertThat(page.getContent()).extracting(User::getUsername).containsExactly("han", "kylo");
+	}
+
+	@Test
+	void testAnnotatedFinderReturningSlice() {
+
+		Slice<User> slice = fragment.findAnnotatedQuerySliceOfUsersByLastname("S",
+				PageRequest.of(0, 2, Sort.by("username")));
+		assertThat(slice.hasNext()).isTrue();
+		assertThat(slice.getSize()).isEqualTo(2);
+		assertThat(slice.getContent()).extracting(User::getUsername).containsExactly("han", "kylo");
+	}
+
+	@Test
+	void testDeleteSingle() {
+
+		User result = fragment.deleteByUsername("yoda");
+
+		assertThat(result).isNotNull().extracting(User::getUsername).isEqualTo("yoda");
+		assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(6L);
+	}
+
+	@Test
+	void testDeleteSingleAnnotatedQuery() {
+
+		User result = fragment.deleteAnnotatedQueryByUsername("yoda");
+
+		assertThat(result).isNotNull().extracting(User::getUsername).isEqualTo("yoda");
+		assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(6L);
+	}
+
+	@Test
+	void testDerivedDeleteMultipleReturningDeleteCount() {
+
+		Long result = fragment.deleteByLastnameStartingWith("S");
+
+		assertThat(result).isEqualTo(4L);
+		assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(3L);
+	}
+
+	@Test
+	void testDerivedDeleteMultipleReturningDeleteCountAnnotatedQuery() {
+
+		Long result = fragment.deleteAnnotatedQueryByLastnameStartingWith("S");
+
+		assertThat(result).isEqualTo(4L);
+		assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(3L);
+	}
+
+	@Test
+	void testDerivedDeleteMultipleReturningDeleted() {
+
+		List<User> result = fragment.deleteUsersByLastnameStartingWith("S");
+
+		assertThat(result).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader");
+		assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(3L);
+	}
+
+	@Test
+	void testDerivedDeleteMultipleReturningDeletedAnnotatedQuery() {
+
+		List<User> result = fragment.deleteUsersAnnotatedQueryByLastnameStartingWith("S");
+
+		assertThat(result).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader");
+		assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(3L);
+	}
+
+	@Test
+	void testDerivedFinderWithAnnotatedSort() {
+
+		List<User> users = fragment.findWithAnnotatedSortByLastnameStartingWith("S");
+		assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader");
+	}
+
+	@Test
+	void testDerivedFinderWithAnnotatedFieldsProjection() {
+
+		List<User> users = fragment.findWithAnnotatedFieldsProjectionByLastnameStartingWith("S");
+		assertThat(users).allMatch(
+				user -> StringUtils.hasText(user.getUsername()) && user.getLastname() == null && user.getFirstname() == null);
+	}
+
+	@Test
+	void testReadPreferenceAppliedToQuery() {
+
+		// check if it fails when trying to parse the read preference to indicate it would get applied
+		assertThatExceptionOfType(IllegalArgumentException.class)
+				.isThrownBy(() -> fragment.findWithReadPreferenceByUsername("S"))
+				.withMessageContaining("No match for read preference");
+	}
+
+	@Test
+	void testDerivedFinderReturningListOfProjections() {
+
+		List<UserProjection> users = fragment.findUserProjectionByLastnameStartingWith("S");
+		assertThat(users).extracting(UserProjection::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader");
+	}
+
+	@Test
+	void testDerivedFinderReturningPageOfProjections() {
+
+		Page<UserProjection> users = fragment.findUserProjectionByLastnameStartingWith("S",
+				PageRequest.of(0, 2, Sort.by("username")));
+		assertThat(users).extracting(UserProjection::getUsername).containsExactly("han", "kylo");
+	}
+
+	@Test
+	void testDerivedFinderReturningPageOfDynamicProjections() {
+
+		Page<UserProjection> users = fragment.findUserProjectionByLastnameStartingWith("S",
+				PageRequest.of(0, 2, Sort.by("username")), UserProjection.class);
+		assertThat(users).extracting(UserProjection::getUsername).containsExactly("han", "kylo");
+	}
+
+	@Test
+	void testUpdateWithDerivedQuery() {
+
+		int modifiedCount = fragment.findUserAndIncrementVisitsByLastname("Organa", 42);
+
+		assertThat(modifiedCount).isOne();
+		assertThat(client.getDatabase(DB_NAME).getCollection("user").find(new Document("_id", "id-2")).first().get("visits",
+				Integer.class)).isEqualTo(42);
+	}
+
+	@Test
+	void testUpdateWithAnnotatedQuery() {
+
+		int modifiedCount = fragment.updateAllByLastname("Organa", 42);
+
+		assertThat(modifiedCount).isOne();
+		assertThat(client.getDatabase(DB_NAME).getCollection("user").find(new Document("_id", "id-2")).first().get("visits",
+				Integer.class)).isEqualTo(42);
+	}
+
+	@Test
+	void testAggregationPipelineUpdate() {
+
+		fragment.findAndIncrementVisitsViaPipelineByLastname("Organa", 42);
+
+		assertThat(client.getDatabase(DB_NAME).getCollection("user").find(new Document("_id", "id-2")).first().get("visits",
+				Integer.class)).isEqualTo(42);
+	}
+
+	@Test
+	void testAggregationWithExtractedSimpleResults() {
+
+		List<String> allLastnames = fragment.findAllLastnames();
+		assertThat(allLastnames).containsExactlyInAnyOrder("Skywalker", "Solo", "Organa", "Solo", "Skywalker");
+	}
+
+	@Test
+	void testAggregationWithProjectedResults() {
+
+		List<UserAggregate> allLastnames = fragment.groupByLastnameAnd("first_name");
+		assertThat(allLastnames).containsExactlyInAnyOrder(//
+				new UserAggregate("Skywalker", List.of("Anakin", "Luke")), //
+				new UserAggregate("Organa", List.of("Leia")), //
+				new UserAggregate("Solo", List.of("Han", "Ben")));
+	}
+
+	@Test
+	void testAggregationWithProjectedResultsLimitedByPageable() {
+
+		List<UserAggregate> allLastnames = fragment.groupByLastnameAnd("first_name", PageRequest.of(1, 1, Sort.by("_id")));
+		assertThat(allLastnames).containsExactly(//
+				new UserAggregate("Skywalker", List.of("Anakin", "Luke")) //
+		);
+	}
+
+	@Test
+	void testAggregationWithProjectedResultsAsPage() {
+
+		Slice<UserAggregate> allLastnames = fragment.groupByLastnameAndReturnPage("first_name",
+				PageRequest.of(1, 1, Sort.by("_id")));
+		assertThat(allLastnames.hasPrevious()).isTrue();
+		assertThat(allLastnames.hasNext()).isTrue();
+		assertThat(allLastnames.getContent()).containsExactly(//
+				new UserAggregate("Skywalker", List.of("Anakin", "Luke")) //
+		);
+	}
+
+	@Test
+	void testAggregationWithProjectedResultsWrappedInAggregationResults() {
+
+		AggregationResults<UserAggregate> allLastnames = fragment.groupByLastnameAndAsAggregationResults("first_name");
+		assertThat(allLastnames.getMappedResults()).containsExactlyInAnyOrder(//
+				new UserAggregate("Skywalker", List.of("Anakin", "Luke")), //
+				new UserAggregate("Organa", List.of("Leia")), //
+				new UserAggregate("Solo", List.of("Han", "Ben")));
+	}
+
+	@Test
+	void testAggregationStreamWithProjectedResultsWrappedInAggregationResults() {
+
+		List<UserAggregate> allLastnames = fragment.streamGroupByLastnameAndAsAggregationResults("first_name").toList();
+		assertThat(allLastnames).containsExactlyInAnyOrder(//
+				new UserAggregate("Skywalker", List.of("Anakin", "Luke")), //
+				new UserAggregate("Organa", List.of("Leia")), //
+				new UserAggregate("Solo", List.of("Han", "Ben")));
+	}
+
+	@Test
+	void testAggregationWithSingleResultExtraction() {
+		assertThat(fragment.sumPosts()).isEqualTo(5);
+	}
+
+	@Test
+	void testAggregationWithHint() {
+		assertThatException().isThrownBy(() -> fragment.findAllLastnamesUsingIndex())
+				.withMessageContaining("hint provided does not correspond to an existing index");
+	}
+
+	@Test
+	void testAggregationWithReadPreference() {
+		assertThatException().isThrownBy(() -> fragment.findAllLastnamesWithReadPreference())
+				.withMessageContaining("No match for read preference");
+	}
+
+	@Test
+	void testAggregationWithCollation() {
+		assertThatException().isThrownBy(() -> fragment.findAllLastnamesWithCollation())
+				.withMessageContaining("'locale' is invalid");
+	}
+
+	private static void initUsers() {
+
+		Document luke = Document.parse("""
+				{
+				  "_id": "id-1",
+				  "username": "luke",
+				  "first_name": "Luke",
+				  "last_name": "Skywalker",
+				  "visits" : 2,
+				  "lastSeen" : {
+				    "$date": "2025-04-01T00:00:00.000Z"
+				   },
+				  "posts": [
+				    {
+				      "message": "I have a bad feeling about this.",
+				      "date": {
+				        "$date": "2025-01-15T12:50:33.855Z"
+				      }
+				    }
+				  ],
+				  "_class": "example.springdata.aot.User"
+				}""");
+
+		Document leia = Document.parse("""
+				{
+				  "_id": "id-2",
+				  "username": "leia",
+				  "first_name": "Leia",
+				  "last_name": "Organa",
+				  "_class": "example.springdata.aot.User"
+				}""");
+
+		Document han = Document.parse("""
+				{
+				  "_id": "id-3",
+				  "username": "han",
+				  "first_name": "Han",
+				  "last_name": "Solo",
+				  "posts": [
+				    {
+				      "message": "It's the ship that made the Kessel Run in less than 12 Parsecs.",
+				      "date": {
+				        "$date": "2025-01-15T13:30:33.855Z"
+				      }
+				    }
+				  ],
+				  "_class": "example.springdata.aot.User"
+				}""");
+
+		Document chwebacca = Document.parse("""
+				{
+				  "_id": "id-4",
+				  "username": "chewbacca",
+				  "lastSeen" : {
+				    "$date": "2025-01-01T00:00:00.000Z"
+				   },
+				  "_class": "example.springdata.aot.User"
+				}""");
+
+		Document yoda = Document.parse(
+				"""
+						{
+						  "_id": "id-5",
+						  "username": "yoda",
+						  "visits" : 1000,
+						  "posts": [
+						    {
+						      "message": "Do. Or do not. There is no try.",
+						      "date": {
+						        "$date": "2025-01-15T13:09:33.855Z"
+						      }
+						    },
+						    {
+						      "message": "Decide you must, how to serve them best. If you leave now, help them you could; but you would destroy all for which they have fought, and suffered.",
+						      "date": {
+						        "$date": "2025-01-15T13:53:33.855Z"
+						      }
+						    }
+						  ]
+						}""");
+
+		Document vader = Document.parse("""
+				{
+				  "_id": "id-6",
+				  "username": "vader",
+				  "first_name": "Anakin",
+				  "last_name": "Skywalker",
+				  "visits" : 50,
+				  "posts": [
+				    {
+				      "message": "I am your father",
+				      "date": {
+				        "$date": "2025-01-15T13:46:33.855Z"
+				      }
+				    }
+				  ]
+				}""");
+
+		Document kylo = Document.parse("""
+				{
+				  "_id": "id-7",
+				  "username": "kylo",
+				  "first_name": "Ben",
+				  "last_name": "Solo"
+				}
+				""");
+
+		client.getDatabase(DB_NAME).getCollection("user")
+				.insertMany(List.of(luke, leia, han, chwebacca, yoda, vader, kylo));
+	}
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorUnitTests.java
new file mode 100644
index 0000000000..e53b3ae679
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorUnitTests.java
@@ -0,0 +1,90 @@
+/*
+ * 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.mongodb.repository.aot;
+
+import static org.mockito.Mockito.*;
+import static org.springframework.data.mongodb.test.util.Assertions.*;
+
+import example.aot.User;
+import example.aot.UserRepository;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.aot.generate.GeneratedFiles;
+import org.springframework.aot.test.generate.TestGenerationContext;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.core.io.InputStreamSource;
+import org.springframework.data.mongodb.core.MongoOperations;
+import org.springframework.data.mongodb.repository.Meta;
+import org.springframework.data.mongodb.test.util.MongoClientExtension;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+/**
+ * Unit tests for the {@link UserRepository} fragment sources via {@link MongoRepositoryContributor}.
+ *
+ * @author Mark Paluch
+ */
+@ExtendWith(MongoClientExtension.class)
+@SpringJUnitConfig(classes = MongoRepositoryContributorUnitTests.MongoRepositoryContributorConfiguration.class)
+class MongoRepositoryContributorUnitTests {
+
+	@Configuration
+	static class MongoRepositoryContributorConfiguration extends AotFragmentTestConfigurationSupport {
+
+		public MongoRepositoryContributorConfiguration() {
+			super(MetaUserRepository.class);
+		}
+
+		@Bean
+		MongoOperations mongoOperations() {
+			return mock(MongoOperations.class);
+		}
+
+	}
+
+	@Autowired TestGenerationContext generationContext;
+
+	@Test
+	void shouldConsiderMetaAnnotation() throws IOException {
+
+		InputStreamSource aotFragment = generationContext.getGeneratedFiles().getGeneratedFile(GeneratedFiles.Kind.SOURCE,
+				MetaUserRepository.class.getPackageName().replace('.', '/') + "/MetaUserRepositoryImpl__Aot.java");
+
+		String content = new InputStreamResource(aotFragment).getContentAsString(StandardCharsets.UTF_8);
+
+		assertThat(content).contains("filterQuery.maxTimeMsec(555)");
+		assertThat(content).contains("filterQuery.cursorBatchSize(1234)");
+		assertThat(content).contains("filterQuery.comment(\"foo\")");
+	}
+
+	interface MetaUserRepository extends CrudRepository<User, String> {
+
+		@Meta
+		User findAllByLastname(String lastname);
+
+		@Meta(cursorBatchSize = 1234, comment = "foo", maxExecutionTimeMs = 555)
+		User findWithMetaAllByLastname(String lastname);
+	}
+
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryMetadataTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryMetadataTests.java
new file mode 100644
index 0000000000..aa069a2710
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryMetadataTests.java
@@ -0,0 +1,192 @@
+/*
+ * 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.mongodb.repository.aot;
+
+import static net.javacrumbs.jsonunit.assertj.JsonAssertions.*;
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import example.aot.UserRepository;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.support.AbstractApplicationContext;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.UrlResource;
+import org.springframework.data.mongodb.core.MongoOperations;
+import org.springframework.data.mongodb.test.util.MongoClientExtension;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+/**
+ * Integration tests for the {@link UserRepository} JSON metadata via {@link MongoRepositoryContributor}.
+ *
+ * @author Mark Paluch
+ */
+@ExtendWith(MongoClientExtension.class)
+@SpringJUnitConfig(classes = MongoRepositoryMetadataTests.MongoRepositoryContributorConfiguration.class)
+class MongoRepositoryMetadataTests {
+
+	@Configuration
+	static class MongoRepositoryContributorConfiguration extends AotFragmentTestConfigurationSupport {
+
+		public MongoRepositoryContributorConfiguration() {
+			super(UserRepository.class);
+		}
+
+		@Bean
+		MongoOperations mongoOperations() {
+			return mock(MongoOperations.class);
+		}
+
+	}
+
+	@Autowired AbstractApplicationContext context;
+
+	@Test // GH-4964
+	void shouldDocumentBase() throws IOException {
+
+		Resource resource = getResource();
+
+		assertThat(resource).isNotNull();
+		assertThat(resource.exists()).isTrue();
+
+		String json = resource.getContentAsString(StandardCharsets.UTF_8);
+
+		assertThatJson(json).isObject() //
+				.containsEntry("name", UserRepository.class.getName()) //
+				.containsEntry("module", "MongoDB") //
+				.containsEntry("type", "IMPERATIVE");
+	}
+
+	@Test // GH-4964
+	void shouldDocumentDerivedQuery() throws IOException {
+
+		Resource resource = getResource();
+
+		assertThat(resource).isNotNull();
+		assertThat(resource.exists()).isTrue();
+
+		String json = resource.getContentAsString(StandardCharsets.UTF_8);
+
+		assertThatJson(json).inPath("$.methods[?(@.name == 'countUsersByLastname')].query").isArray().element(0).isObject()
+				.containsEntry("filter", "{'lastname':?0}");
+	}
+
+	@Test // GH-4964
+	void shouldDocumentSortedQuery() throws IOException {
+
+		Resource resource = getResource();
+
+		assertThat(resource).isNotNull();
+		assertThat(resource.exists()).isTrue();
+
+		String json = resource.getContentAsString(StandardCharsets.UTF_8);
+
+		assertThatJson(json).inPath("$.methods[?(@.name == 'findByLastnameStartingWithOrderByUsername')].query") //
+				.isArray().element(0).isObject() //
+				.containsEntry("filter", "{'lastname':{'$regex':/^\\Q?0\\E/}}")
+				.containsEntry("sort", "{'username':{'$numberInt':'1'}}");
+	}
+
+	@Test // GH-4964
+	void shouldDocumentPagedQuery() throws IOException {
+
+		Resource resource = getResource();
+
+		assertThat(resource).isNotNull();
+		assertThat(resource.exists()).isTrue();
+
+		String json = resource.getContentAsString(StandardCharsets.UTF_8);
+
+		assertThatJson(json).inPath("$.methods[?(@.name == 'findPageOfUsersByLastnameStartingWith')].query").isArray()
+				.element(0).isObject().containsEntry("filter", "{'lastname':{'$regex':/^\\Q?0\\E/}}");
+	}
+
+	@Test // GH-4964
+	@Disabled("No support for expressions yet")
+	void shouldDocumentQueryWithExpression() throws IOException {
+
+		Resource resource = getResource();
+
+		assertThat(resource).isNotNull();
+		assertThat(resource.exists()).isTrue();
+
+		String json = resource.getContentAsString(StandardCharsets.UTF_8);
+
+		assertThatJson(json).inPath("$.methods[?(@.name == 'findValueExpressionNamedByEmailAddress')].query").isArray()
+				.first().isObject().containsEntry("query", "select u from User u where u.emailAddress = :__$synthetic$__1");
+	}
+
+	@Test // GH-4964
+	void shouldDocumentAggregation() throws IOException {
+
+		Resource resource = getResource();
+
+		assertThat(resource).isNotNull();
+		assertThat(resource.exists()).isTrue();
+
+		String json = resource.getContentAsString(StandardCharsets.UTF_8);
+
+		assertThatJson(json).inPath("$.methods[?(@.name == 'findAllLastnames')].query").isArray().element(0).isObject()
+				.containsEntry("pipeline",
+						"[{ '$match' : { 'last_name' : { '$ne' : null } } }, { '$project': { '_id' : '$last_name' } }]");
+	}
+
+	@Test // GH-4964
+	void shouldDocumentPipelineUpdate() throws IOException {
+
+		Resource resource = getResource();
+
+		assertThat(resource).isNotNull();
+		assertThat(resource.exists()).isTrue();
+
+		String json = resource.getContentAsString(StandardCharsets.UTF_8);
+
+		assertThatJson(json).inPath("$.methods[?(@.name == 'findAndIncrementVisitsViaPipelineByLastname')].query").isArray()
+				.element(0).isObject().containsEntry("filter", "{'lastname':?0}").containsEntry("update-pipeline",
+						"[{ '$set' : { 'visits' : { '$ifNull' : [ {'$add' : [ '$visits', ?1 ] }, ?1 ] } } }]");
+	}
+
+	@Test // GH-4964
+	void shouldDocumentBaseFragment() throws IOException {
+
+		Resource resource = getResource();
+
+		assertThat(resource).isNotNull();
+		assertThat(resource.exists()).isTrue();
+
+		String json = resource.getContentAsString(StandardCharsets.UTF_8);
+
+		assertThatJson(json).inPath("$.methods[?(@.name == 'existsById')].fragment").isArray().first().isObject()
+				.containsEntry("fragment", "org.springframework.data.mongodb.repository.support.SimpleMongoRepository");
+	}
+
+	private Resource getResource() {
+
+		String location = UserRepository.class.getPackageName().replace('.', '/') + "/"
+				+ UserRepository.class.getSimpleName() + ".json";
+		return new UrlResource(context.getBeanFactory().getBeanClassLoader().getResource(location));
+	}
+
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/ReactiveAotContributionIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/ReactiveAotContributionIntegrationTests.java
new file mode 100644
index 0000000000..24b89345a8
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/ReactiveAotContributionIntegrationTests.java
@@ -0,0 +1,116 @@
+/*
+ * 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.mongodb.repository.aot;
+
+import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
+import static org.mockito.Mockito.mock;
+
+import example.aot.User;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.aot.generate.GeneratedFiles;
+import org.springframework.aot.test.generate.TestGenerationContext;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.FilterType;
+import org.springframework.context.aot.ApplicationContextAotGenerator;
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.core.io.InputStreamSource;
+import org.springframework.data.aot.AotContext;
+import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
+import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories;
+import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.mock.env.MockPropertySource;
+
+import com.mongodb.client.MongoClient;
+
+/**
+ * Integration tests for AOT processing of reactive repositories.
+ *
+ * @author Mark Paluch
+ */
+class ReactiveAotContributionIntegrationTests {
+
+	@EnableReactiveMongoRepositories(considerNestedRepositories = true, includeFilters = {
+			@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = ReactiveQuerydslUserRepository.class) })
+	static class AotConfiguration extends AbstractMongoClientConfiguration {
+
+		@Override
+		public MongoClient mongoClient() {
+			return mock(MongoClient.class);
+		}
+
+		@Override
+		protected String getDatabaseName() {
+			return "";
+		}
+	}
+
+	interface ReactiveQuerydslUserRepository
+			extends CrudRepository<User, String>, ReactiveQuerydslPredicateExecutor<User> {
+
+		Flux<User> findUserNoArgumentsBy();
+
+		Mono<User> findOneByUsername(String username);
+
+	}
+
+	@Test // GH-4964
+	void shouldGenerateMetadataForBaseRepositoryAndQuerydslFragment() throws IOException {
+
+		TestGenerationContext generationContext = generate(AotConfiguration.class);
+
+		InputStreamSource metadata = generationContext.getGeneratedFiles().getGeneratedFile(GeneratedFiles.Kind.RESOURCE,
+				ReactiveQuerydslUserRepository.class.getName().replace('.', '/') + ".json");
+
+		InputStreamResource isr = new InputStreamResource(metadata);
+		String json = isr.getContentAsString(StandardCharsets.UTF_8);
+
+		assertThatJson(json).isObject() //
+				.containsEntry("name", ReactiveQuerydslUserRepository.class.getName()) //
+				.containsEntry("module", "MongoDB") //
+				.containsEntry("type", "REACTIVE");
+
+		assertThatJson(json).inPath("$.methods[?(@.name == 'findBy')].fragment").isArray().first().isObject()
+				.containsEntry("interface", "org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor")
+				.containsEntry("fragment",
+						"org.springframework.data.mongodb.repository.support.ReactiveQuerydslMongoPredicateExecutor");
+
+		assertThatJson(json).inPath("$.methods[?(@.name == 'existsById')].fragment").isArray().first().isObject()
+				.containsEntry("fragment", "org.springframework.data.mongodb.repository.support.SimpleReactiveMongoRepository");
+	}
+
+	private static TestGenerationContext generate(Class<?>... configurationClasses) {
+
+		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
+		context.getEnvironment().getPropertySources()
+				.addFirst(new MockPropertySource().withProperty(AotContext.GENERATED_REPOSITORIES_ENABLED, "true"));
+		context.register(configurationClasses);
+
+		ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator();
+
+		TestGenerationContext generationContext = new TestGenerationContext();
+		generator.processAheadOfTime(context, generationContext);
+		return generationContext;
+	}
+
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/StubRepositoryInformation.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/StubRepositoryInformation.java
new file mode 100644
index 0000000000..7092848c33
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/StubRepositoryInformation.java
@@ -0,0 +1,140 @@
+/*
+ * 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.mongodb.repository.aot;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Set;
+
+import org.jspecify.annotations.Nullable;
+import org.springframework.data.mongodb.repository.support.SimpleMongoRepository;
+import org.springframework.data.repository.core.CrudMethods;
+import org.springframework.data.repository.core.RepositoryInformation;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.core.support.AbstractRepositoryMetadata;
+import org.springframework.data.repository.core.support.RepositoryComposition;
+import org.springframework.data.repository.core.support.RepositoryFragment;
+import org.springframework.data.util.TypeInformation;
+
+/**
+ * @author Christoph Strobl
+ */
+class StubRepositoryInformation implements RepositoryInformation {
+
+	private final RepositoryMetadata metadata;
+	private final RepositoryComposition baseComposition;
+
+	public StubRepositoryInformation(Class<?> repositoryInterface, @Nullable RepositoryComposition composition) {
+
+		this.metadata = AbstractRepositoryMetadata.getMetadata(repositoryInterface);
+		this.baseComposition = composition != null ? composition
+				: RepositoryComposition.of(RepositoryFragment.structural(SimpleMongoRepository.class));
+	}
+
+	@Override
+	public TypeInformation<?> getIdTypeInformation() {
+		return metadata.getIdTypeInformation();
+	}
+
+	@Override
+	public TypeInformation<?> getDomainTypeInformation() {
+		return metadata.getDomainTypeInformation();
+	}
+
+	@Override
+	public Class<?> getRepositoryInterface() {
+		return metadata.getRepositoryInterface();
+	}
+
+	@Override
+	public TypeInformation<?> getReturnType(Method method) {
+		return metadata.getReturnType(method);
+	}
+
+	@Override
+	public Class<?> getReturnedDomainClass(Method method) {
+		return metadata.getReturnedDomainClass(method);
+	}
+
+	@Override
+	public TypeInformation<?> getReturnedDomainTypeInformation(Method method) {
+		return metadata.getReturnedDomainTypeInformation(method);
+	}
+
+	@Override
+	public CrudMethods getCrudMethods() {
+		return metadata.getCrudMethods();
+	}
+
+	@Override
+	public boolean isPagingRepository() {
+		return false;
+	}
+
+	@Override
+	public Set<Class<?>> getAlternativeDomainTypes() {
+		return null;
+	}
+
+	@Override
+	public boolean isReactiveRepository() {
+		return false;
+	}
+
+	@Override
+	public Set<RepositoryFragment<?>> getFragments() {
+		return null;
+	}
+
+	@Override
+	public boolean isBaseClassMethod(Method method) {
+		return baseComposition.findMethod(method).isPresent();
+	}
+
+	@Override
+	public boolean isCustomMethod(Method method) {
+		return false;
+	}
+
+	@Override
+	public boolean isQueryMethod(Method method) {
+		if (isBaseClassMethod(method)) {
+			return false;
+		}
+
+		return true;
+	}
+
+	@Override
+	public List<Method> getQueryMethods() {
+		return null;
+	}
+
+	@Override
+	public Class<?> getRepositoryBaseClass() {
+		return SimpleMongoRepository.class;
+	}
+
+	@Override
+	public Method getTargetClassMethod(Method method) {
+		return null;
+	}
+
+	@Override
+	public RepositoryComposition getRepositoryComposition() {
+		return baseComposition;
+	}
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java
new file mode 100644
index 0000000000..5f470dd550
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java
@@ -0,0 +1,124 @@
+/*
+ * 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.mongodb.repository.aot;
+
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+import java.util.List;
+import java.util.Set;
+
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.core.annotation.MergedAnnotation;
+import org.springframework.core.env.Environment;
+import org.springframework.core.env.StandardEnvironment;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.test.tools.ClassFile;
+import org.springframework.data.mongodb.core.mapping.Document;
+import org.springframework.data.repository.config.AotRepositoryContext;
+import org.springframework.data.repository.config.RepositoryConfigurationSource;
+import org.springframework.data.repository.core.RepositoryInformation;
+import org.springframework.data.repository.core.support.RepositoryComposition;
+
+/**
+ * @author Christoph Strobl
+ */
+public class TestMongoAotRepositoryContext implements AotRepositoryContext {
+
+	private final StubRepositoryInformation repositoryInformation;
+	private final Environment environment = new StandardEnvironment();
+
+	public TestMongoAotRepositoryContext(Class<?> repositoryInterface, @Nullable RepositoryComposition composition) {
+		this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition);
+	}
+
+	@Override
+	public ConfigurableListableBeanFactory getBeanFactory() {
+		return null;
+	}
+
+	@Override
+	public TypeIntrospector introspectType(String typeName) {
+		return null;
+	}
+
+	@Override
+	public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) {
+		return null;
+	}
+
+	@Override
+	public String getBeanName() {
+		return "dummyRepository";
+	}
+
+	@Override
+	public String getModuleName() {
+		return "MongoDB";
+	}
+
+	@Override
+	public RepositoryConfigurationSource getConfigurationSource() {
+		return null;
+	}
+
+	@Override
+	public Set<String> getBasePackages() {
+		return Set.of("org.springframework.data.dummy.repository.aot");
+	}
+
+	@Override
+	public Set<Class<? extends Annotation>> getIdentifyingAnnotations() {
+		return Set.of(Document.class);
+	}
+
+	@Override
+	public RepositoryInformation getRepositoryInformation() {
+		return repositoryInformation;
+	}
+
+	@Override
+	public Set<MergedAnnotation<Annotation>> getResolvedAnnotations() {
+		return Set.of();
+	}
+
+	@Override
+	public Set<Class<?>> getResolvedTypes() {
+		return Set.of();
+	}
+
+	public List<ClassFile> getRequiredContextFiles() {
+		return List.of(classFileForType(repositoryInformation.getRepositoryBaseClass()));
+	}
+
+	static ClassFile classFileForType(Class<?> type) {
+
+		String name = type.getName();
+		ClassPathResource cpr = new ClassPathResource(name.replaceAll("\\.", "/") + ".class");
+
+		try {
+			return ClassFile.of(name, cpr.getContentAsByteArray());
+		} catch (IOException e) {
+			throw new IllegalArgumentException("Cannot open [%s].".formatted(cpr.getPath()));
+		}
+	}
+
+	@Override
+	public Environment getEnvironment() {
+		return environment;
+	}
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/config/MongoRepositoryConfigurationExtensionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/config/MongoRepositoryConfigurationExtensionUnitTests.java
index f613beb6d5..a222deca39 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/config/MongoRepositoryConfigurationExtensionUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/config/MongoRepositoryConfigurationExtensionUnitTests.java
@@ -27,7 +27,7 @@
 import org.springframework.core.env.StandardEnvironment;
 import org.springframework.core.io.ResourceLoader;
 import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
-import org.springframework.core.type.StandardAnnotationMetadata;
+import org.springframework.core.type.AnnotationMetadata;
 import org.springframework.data.mongodb.core.mapping.Document;
 import org.springframework.data.mongodb.repository.MongoRepository;
 import org.springframework.data.repository.Repository;
@@ -43,13 +43,13 @@
  */
 public class MongoRepositoryConfigurationExtensionUnitTests {
 
-	StandardAnnotationMetadata metadata = new StandardAnnotationMetadata(Config.class, true);
+	AnnotationMetadata metadata = AnnotationMetadata.introspect(Config.class);
 	ResourceLoader loader = new PathMatchingResourcePatternResolver();
 	Environment environment = new StandardEnvironment();
 	BeanDefinitionRegistry registry = new DefaultListableBeanFactory();
 
 	RepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource(metadata,
-			EnableMongoRepositories.class, loader, environment, registry);
+			EnableMongoRepositories.class, loader, environment, registry, null);
 
 	@Test // DATAMONGO-1009
 	public void isStrictMatchIfDomainTypeIsAnnotatedWithDocument() {
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/config/ReactiveMongoRepositoryConfigurationExtensionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/config/ReactiveMongoRepositoryConfigurationExtensionUnitTests.java
index 45ecba992f..2b52204f74 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/config/ReactiveMongoRepositoryConfigurationExtensionUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/config/ReactiveMongoRepositoryConfigurationExtensionUnitTests.java
@@ -27,7 +27,7 @@
 import org.springframework.core.env.StandardEnvironment;
 import org.springframework.core.io.ResourceLoader;
 import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
-import org.springframework.core.type.StandardAnnotationMetadata;
+import org.springframework.core.type.AnnotationMetadata;
 import org.springframework.data.mongodb.core.mapping.Document;
 import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
 import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource;
@@ -43,13 +43,13 @@
  */
 public class ReactiveMongoRepositoryConfigurationExtensionUnitTests {
 
-	StandardAnnotationMetadata metadata = new StandardAnnotationMetadata(Config.class, true);
+	AnnotationMetadata metadata = AnnotationMetadata.introspect(Config.class);
 	ResourceLoader loader = new PathMatchingResourcePatternResolver();
 	Environment environment = new StandardEnvironment();
 	BeanDefinitionRegistry registry = new DefaultListableBeanFactory();
 
 	RepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource(metadata,
-			EnableReactiveMongoRepositories.class, loader, environment, registry);
+			EnableReactiveMongoRepositories.class, loader, environment, registry, null);
 
 	@Test // DATAMONGO-1444
 	public void isStrictMatchIfDomainTypeIsAnnotatedWithDocument() {
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java
index ea3c9ad023..dad28ae5aa 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java
@@ -51,6 +51,7 @@
 import org.springframework.data.mongodb.MongoDatabaseFactory;
 import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind;
 import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery;
+import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove;
 import org.springframework.data.mongodb.core.ExecutableUpdateOperation.ExecutableUpdate;
 import org.springframework.data.mongodb.core.ExecutableUpdateOperation.TerminatingUpdate;
 import org.springframework.data.mongodb.core.ExecutableUpdateOperation.UpdateWithQuery;
@@ -104,6 +105,7 @@ class AbstractMongoQueryUnitTests {
 	@Mock UpdateWithQuery updateWithQuery;
 	@Mock UpdateWithUpdate updateWithUpdate;
 	@Mock TerminatingUpdate terminatingUpdate;
+	@Mock ExecutableRemove executableRemove;
 	@Mock BasicMongoPersistentEntity<?> persitentEntityMock;
 	@Mock MongoMappingContext mappingContextMock;
 	@Mock DeleteResult deleteResultMock;
@@ -130,8 +132,9 @@ void setUp() {
 		doReturn(executableUpdate).when(mongoOperationsMock).update(any());
 		doReturn(updateWithQuery).when(executableUpdate).matching(any(Query.class));
 		doReturn(terminatingUpdate).when(updateWithQuery).apply(any(UpdateDefinition.class));
-
-		when(mongoOperationsMock.remove(any(), any(), anyString())).thenReturn(deleteResultMock);
+		doReturn(executableRemove).when(mongoOperationsMock).remove(any());
+		doReturn(executableRemove).when(executableRemove).matching(any(Query.class));
+		when(executableRemove.all()).thenReturn(deleteResultMock);
 		when(mongoOperationsMock.updateMulti(any(), any(), any(), anyString())).thenReturn(updateResultMock);
 	}
 
@@ -140,8 +143,7 @@ void testDeleteExecutionCallsRemoveCorrectly() {
 
 		createQueryForMethod("deletePersonByLastname", String.class).setDeleteQuery(true).execute(new Object[] { "booh" });
 
-		verify(mongoOperationsMock, times(1)).remove(any(), eq(Person.class), eq("persons"));
-		verify(mongoOperationsMock, times(0)).find(any(), any(), any());
+		verify(executableRemove, times(1)).all();
 	}
 
 	@Test // DATAMONGO-566, DATAMONGO-1040
@@ -149,7 +151,7 @@ void testDeleteExecutionLoadsListOfRemovedDocumentsWhenReturnTypeIsCollectionLik
 
 		createQueryForMethod("deleteByLastname", String.class).setDeleteQuery(true).execute(new Object[] { "booh" });
 
-		verify(mongoOperationsMock, times(1)).findAllAndRemove(any(), eq(Person.class), eq("persons"));
+		verify(executableRemove, times(1)).findAndRemove();
 	}
 
 	@Test // DATAMONGO-566
@@ -171,7 +173,7 @@ void testDeleteExecutionReturnsNrDocumentsDeletedFromWriteResult() {
 		query.setDeleteQuery(true);
 
 		assertThat(query.execute(new Object[] { "fake" })).isEqualTo(100L);
-		verify(mongoOperationsMock, times(1)).remove(any(), eq(Person.class), eq("persons"));
+		verify(executableRemove, times(1)).all();
 	}
 
 	@Test // DATAMONGO-957
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java
index 1c856394d8..f0ffebde20 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java
@@ -22,8 +22,10 @@
 
 import org.bson.Document;
 import org.junit.jupiter.api.Test;
+
 import org.springframework.data.domain.Range;
 import org.springframework.data.domain.Range.Bound;
+import org.springframework.data.domain.Score;
 import org.springframework.data.geo.Distance;
 import org.springframework.data.geo.Metrics;
 import org.springframework.data.geo.Point;
@@ -45,15 +47,15 @@
  * @author Oliver Gierke
  * @author Christoph Strobl
  */
-public class MongoParametersParameterAccessorUnitTests {
+class MongoParametersParameterAccessorUnitTests {
 
-	Distance DISTANCE = new Distance(2.5, Metrics.KILOMETERS);
-	RepositoryMetadata metadata = new DefaultRepositoryMetadata(PersonRepository.class);
-	MongoMappingContext context = new MongoMappingContext();
-	ProjectionFactory factory = new SpelAwareProxyProjectionFactory();
+	private Distance DISTANCE = Distance.of(2.5, Metrics.KILOMETERS);
+	private RepositoryMetadata metadata = new DefaultRepositoryMetadata(PersonRepository.class);
+	private MongoMappingContext context = new MongoMappingContext();
+	private ProjectionFactory factory = new SpelAwareProxyProjectionFactory();
 
 	@Test
-	public void returnsUnboundedForDistanceIfNoneAvailable() throws NoSuchMethodException, SecurityException {
+	void returnsUnboundedForDistanceIfNoneAvailable() throws NoSuchMethodException, SecurityException {
 
 		Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class);
 		MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context);
@@ -64,7 +66,7 @@ public void returnsUnboundedForDistanceIfNoneAvailable() throws NoSuchMethodExce
 	}
 
 	@Test
-	public void returnsDistanceIfAvailable() throws NoSuchMethodException, SecurityException {
+	void returnsDistanceIfAvailable() throws NoSuchMethodException, SecurityException {
 
 		Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Distance.class);
 		MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context);
@@ -75,7 +77,7 @@ public void returnsDistanceIfAvailable() throws NoSuchMethodException, SecurityE
 	}
 
 	@Test // DATAMONGO-973
-	public void shouldReturnAsFullTextStringWhenNoneDefinedForMethod() throws NoSuchMethodException, SecurityException {
+	void shouldReturnAsFullTextStringWhenNoneDefinedForMethod() throws NoSuchMethodException, SecurityException {
 
 		Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Distance.class);
 		MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context);
@@ -86,7 +88,7 @@ public void shouldReturnAsFullTextStringWhenNoneDefinedForMethod() throws NoSuch
 	}
 
 	@Test // DATAMONGO-973
-	public void shouldProperlyConvertTextCriteria() throws NoSuchMethodException, SecurityException {
+	void shouldProperlyConvertTextCriteria() throws NoSuchMethodException, SecurityException {
 
 		Method method = PersonRepository.class.getMethod("findByFirstname", String.class, TextCriteria.class);
 		MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context);
@@ -98,13 +100,13 @@ public void shouldProperlyConvertTextCriteria() throws NoSuchMethodException, Se
 	}
 
 	@Test // DATAMONGO-1110
-	public void shouldDetectMinAndMaxDistance() throws NoSuchMethodException, SecurityException {
+	void shouldDetectMinAndMaxDistance() throws NoSuchMethodException, SecurityException {
 
 		Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Range.class);
 		MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context);
 
-		Distance min = new Distance(10, Metrics.KILOMETERS);
-		Distance max = new Distance(20, Metrics.KILOMETERS);
+		Distance min = Distance.of(10, Metrics.KILOMETERS);
+		Distance max = Distance.of(20, Metrics.KILOMETERS);
 
 		MongoParameterAccessor accessor = new MongoParametersParameterAccessor(queryMethod,
 				new Object[] { new Point(10, 20), Distance.between(min, max) });
@@ -116,7 +118,7 @@ public void shouldDetectMinAndMaxDistance() throws NoSuchMethodException, Securi
 	}
 
 	@Test // DATAMONGO-1854
-	public void shouldDetectCollation() throws NoSuchMethodException, SecurityException {
+	void shouldDetectCollation() throws NoSuchMethodException, SecurityException {
 
 		Method method = PersonRepository.class.getMethod("findByFirstname", String.class, Collation.class);
 		MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context);
@@ -129,7 +131,7 @@ public void shouldDetectCollation() throws NoSuchMethodException, SecurityExcept
 	}
 
 	@Test // GH-2107
-	public void shouldReturnUpdateIfPresent() throws NoSuchMethodException, SecurityException {
+	void shouldReturnUpdateIfPresent() throws NoSuchMethodException, SecurityException {
 
 		Method method = PersonRepository.class.getMethod("findAndModifyByFirstname", String.class, UpdateDefinition.class);
 		MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context);
@@ -142,7 +144,7 @@ public void shouldReturnUpdateIfPresent() throws NoSuchMethodException, Security
 	}
 
 	@Test // GH-2107
-	public void shouldReturnNullIfNoUpdatePresent() throws NoSuchMethodException, SecurityException {
+	void shouldReturnNullIfNoUpdatePresent() throws NoSuchMethodException, SecurityException {
 
 		Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class);
 		MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context);
@@ -153,6 +155,23 @@ public void shouldReturnNullIfNoUpdatePresent() throws NoSuchMethodException, Se
 		assertThat(accessor.getUpdate()).isNull();
 	}
 
+	@Test // GH-
+	void shouldReturnRangeFromScore() throws NoSuchMethodException, SecurityException {
+
+		Method method = PersonRepository.class.getMethod("findByFirstname", String.class, Score.class);
+		MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context);
+
+		MongoParameterAccessor accessor = new MongoParametersParameterAccessor(queryMethod,
+				new Object[] { "foo", Score.of(1) });
+
+		Range<Score> scoreRange = accessor.getScoreRange();
+
+		assertThat(scoreRange).isNotNull();
+		assertThat(scoreRange.getLowerBound().isBounded()).isFalse();
+		assertThat(scoreRange.getUpperBound().isBounded()).isTrue();
+		assertThat(scoreRange.getUpperBound().getValue()).contains(Score.of(1));
+	}
+
 	interface PersonRepository extends Repository<Person, Long> {
 
 		List<Person> findByLocationNear(Point point);
@@ -165,6 +184,8 @@ interface PersonRepository extends Repository<Person, Long> {
 
 		List<Person> findByFirstname(String firstname, Collation collation);
 
+		List<Person> findByFirstname(String firstname, Score score);
+
 		List<Person> findAndModifyByFirstname(String firstname, UpdateDefinition update);
 
 	}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java
index 93674e23fc..fc1ffb971e 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java
@@ -27,6 +27,8 @@
 
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.Range;
+import org.springframework.data.domain.Score;
+import org.springframework.data.domain.Vector;
 import org.springframework.data.geo.Distance;
 import org.springframework.data.geo.GeoResults;
 import org.springframework.data.geo.Point;
@@ -43,6 +45,7 @@
  *
  * @author Oliver Gierke
  * @author Christoph Strobl
+ * @author Mark Paluch
  */
 @ExtendWith(MockitoExtension.class)
 class MongoParametersUnitTests {
@@ -184,6 +187,21 @@ void shouldReturnInvalidIndexIfUpdateDoesNotExist() throws NoSuchMethodException
 		assertThat(parameters.getUpdateIndex()).isEqualTo(-1);
 	}
 
+	@Test // GH-2107
+	void shouldOmitVector() throws NoSuchMethodException, SecurityException {
+
+		Method method = PersonRepository.class.getMethod("shouldOmitVector", Vector.class, Score.class,
+				Range.class, String.class);
+		MongoParameters parameters = new MongoParameters(ParametersSource.of(method), false);
+
+		assertThat(parameters.getVectorIndex()).isEqualTo(0);
+		assertThat(parameters.getScoreIndex()).isEqualTo(1);
+		assertThat(parameters.getScoreRangeIndex()).isEqualTo(2);
+
+		MongoParameters bindableParameters = parameters.getBindableParameters();
+		assertThat(bindableParameters).hasSize(3);
+	}
+
 	interface PersonRepository {
 
 		List<Person> findByLocationNear(Point point, Distance distance);
@@ -205,5 +223,8 @@ interface PersonRepository {
 		List<Person> findByText(String text, Collation collation);
 
 		List<Person> findAndModifyByFirstname(String firstname, UpdateDefinition update, Pageable page);
+
+		List<Person> shouldOmitVector(Vector vector, Score distance, Range<Score> range,
+				String country);
 	}
 }
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java
index 609e0a0018..55e3df6b43 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java
@@ -29,6 +29,7 @@
 import org.bson.types.ObjectId;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+
 import org.springframework.data.domain.Range;
 import org.springframework.data.domain.Range.Bound;
 import org.springframework.data.geo.Distance;
@@ -120,7 +121,7 @@ void createsIsNullQueryCorrectly() {
 	void bindsMetricDistanceParameterToNearSphereCorrectly() throws Exception {
 
 		Point point = new Point(10, 20);
-		Distance distance = new Distance(2.5, Metrics.KILOMETERS);
+		Distance distance = Distance.of(2.5, Metrics.KILOMETERS);
 
 		Query query = query(
 				where("location").nearSphere(point).maxDistance(distance.getNormalizedValue()).and("firstname").is("Dave"));
@@ -131,7 +132,7 @@ void bindsMetricDistanceParameterToNearSphereCorrectly() throws Exception {
 	void bindsDistanceParameterToNearCorrectly() throws Exception {
 
 		Point point = new Point(10, 20);
-		Distance distance = new Distance(2.5);
+		Distance distance = Distance.of(2.5);
 
 		Query query = query(
 				where("location").near(point).maxDistance(distance.getNormalizedValue()).and("firstname").is("Dave"));
@@ -405,7 +406,7 @@ void shouldCreateRegexWhenUsingNotContainsOnStringProperty() {
 	void createsNonSphericalNearForDistanceWithDefaultMetric() {
 
 		Point point = new Point(1.0, 1.0);
-		Distance distance = new Distance(1.0);
+		Distance distance = Distance.of(1.0);
 
 		PartTree tree = new PartTree("findByLocationNear", Venue.class);
 		MongoQueryCreator creator = new MongoQueryCreator(tree, getAccessor(converter, point, distance), context);
@@ -445,7 +446,7 @@ void shouldCreateNearSphereQueryForSphericalProperty() {
 	void shouldCreateNearSphereQueryForSphericalPropertyHavingDistanceWithDefaultMetric() {
 
 		Point point = new Point(1.0, 1.0);
-		Distance distance = new Distance(1.0);
+		Distance distance = Distance.of(1.0);
 
 		PartTree tree = new PartTree("findByAddress2dSphere_GeoNear", User.class);
 		MongoQueryCreator creator = new MongoQueryCreator(tree, getAccessor(converter, point, distance), context);
@@ -458,7 +459,7 @@ void shouldCreateNearSphereQueryForSphericalPropertyHavingDistanceWithDefaultMet
 	void shouldCreateNearQueryForMinMaxDistance() {
 
 		Point point = new Point(10, 20);
-		Range<Distance> range = Distance.between(new Distance(10), new Distance(20));
+		Range<Distance> range = Distance.between(Distance.of(10), Distance.of(20));
 
 		PartTree tree = new PartTree("findByAddress_GeoNear", User.class);
 		MongoQueryCreator creator = new MongoQueryCreator(tree, getAccessor(converter, point, range), context);
@@ -664,7 +665,7 @@ void nearShouldUseMetricDistanceForGeoJsonTypes() {
 		GeoJsonPoint point = new GeoJsonPoint(27.987901, 86.9165379);
 		PartTree tree = new PartTree("findByLocationNear", User.class);
 		MongoQueryCreator creator = new MongoQueryCreator(tree,
-				getAccessor(converter, point, new Distance(1, Metrics.KILOMETERS)), context);
+				getAccessor(converter, point, Distance.of(1, Metrics.KILOMETERS)), context);
 
 		assertThat(creator.createQuery()).isEqualTo(query(where("location").nearSphere(point).maxDistance(1000.0D)));
 	}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryExecutionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryExecutionUnitTests.java
index 74ff20b148..2c0c996bc3 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryExecutionUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryExecutionUnitTests.java
@@ -15,13 +15,17 @@
  */
 package org.springframework.data.mongodb.repository.query;
 
-import static org.assertj.core.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import java.lang.reflect.Method;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -41,6 +45,7 @@
 import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery;
 import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind;
 import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFindNear;
+import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove;
 import org.springframework.data.mongodb.core.MongoOperations;
 import org.springframework.data.mongodb.core.convert.DbRefResolver;
 import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
@@ -56,8 +61,7 @@
 import org.springframework.data.repository.Repository;
 import org.springframework.data.repository.core.RepositoryMetadata;
 import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
-import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
-import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.data.repository.query.ValueExpressionDelegate;
 import org.springframework.util.ReflectionUtils;
 
 import com.mongodb.client.result.DeleteResult;
@@ -79,17 +83,16 @@ class MongoQueryExecutionUnitTests {
 	@Mock FindWithQuery<Object> operationMock;
 	@Mock TerminatingFind<Object> terminatingMock;
 	@Mock TerminatingFindNear<Object> terminatingGeoMock;
+	@Mock ExecutableRemove<Object> removeMock;
 	@Mock DbRefResolver dbRefResolver;
 
-	private SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
 	private Point POINT = new Point(10, 20);
-	private Distance DISTANCE = new Distance(2.5, Metrics.KILOMETERS);
+	private Distance DISTANCE = Distance.of(2.5, Metrics.KILOMETERS);
 	private RepositoryMetadata metadata = new DefaultRepositoryMetadata(PersonRepository.class);
 	private MongoMappingContext context = new MongoMappingContext();
 	private ProjectionFactory factory = new SpelAwareProxyProjectionFactory();
 	private Method method = ReflectionUtils.findMethod(PersonRepository.class, "findByLocationNear", Point.class,
-			Distance.class,
-			Pageable.class);
+			Distance.class, Pageable.class);
 	private MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context);
 	private MappingMongoConverter converter;
 
@@ -152,8 +155,8 @@ void pagingGeoExecutionShouldUseCountFromResultWithOffsetAndResultsWithinPageSiz
 		ConvertingParameterAccessor accessor = new ConvertingParameterAccessor(converter,
 				new MongoParametersParameterAccessor(queryMethod, new Object[] { POINT, DISTANCE, PageRequest.of(0, 10) }));
 
-		PartTreeMongoQuery query = new PartTreeMongoQuery(queryMethod, mongoOperationsMock, EXPRESSION_PARSER,
-				QueryMethodEvaluationContextProvider.DEFAULT);
+		PartTreeMongoQuery query = new PartTreeMongoQuery(queryMethod, mongoOperationsMock,
+				ValueExpressionDelegate.create());
 
 		PagingGeoNearExecution execution = new PagingGeoNearExecution(findOperationMock, queryMethod, accessor, query);
 		execution.execute(new Query());
@@ -173,8 +176,8 @@ void pagingGeoExecutionRetrievesObjectsForPageableOutOfRange() {
 		ConvertingParameterAccessor accessor = new ConvertingParameterAccessor(converter,
 				new MongoParametersParameterAccessor(queryMethod, new Object[] { POINT, DISTANCE, PageRequest.of(2, 10) }));
 
-		PartTreeMongoQuery query = new PartTreeMongoQuery(queryMethod, mongoOperationsMock, EXPRESSION_PARSER,
-				QueryMethodEvaluationContextProvider.DEFAULT);
+		PartTreeMongoQuery query = new PartTreeMongoQuery(queryMethod, mongoOperationsMock,
+				ValueExpressionDelegate.create());
 
 		PagingGeoNearExecution execution = new PagingGeoNearExecution(findOperationMock, queryMethod, accessor, query);
 		execution.execute(new Query());
@@ -186,38 +189,36 @@ void pagingGeoExecutionRetrievesObjectsForPageableOutOfRange() {
 	@Test // DATAMONGO-2351
 	void acknowledgedDeleteReturnsDeletedCount() {
 
+		doReturn(removeMock).when(removeMock).matching(any(Query.class));
+		when(removeMock.all()).thenReturn(DeleteResult.acknowledged(10));
 		Method method = ReflectionUtils.findMethod(PersonRepository.class, "deleteAllByLastname", String.class);
 		MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context);
 
-		when(mongoOperationsMock.remove(any(Query.class), any(Class.class), anyString()))
-				.thenReturn(DeleteResult.acknowledged(10));
-
-		assertThat(new DeleteExecution(mongoOperationsMock, queryMethod).execute(new Query())).isEqualTo(10L);
+		assertThat(new DeleteExecution(removeMock, queryMethod).execute(new Query())).isEqualTo(10L);
 	}
 
 	@Test // DATAMONGO-2351
 	void unacknowledgedDeleteReturnsZeroDeletedCount() {
 
+		doReturn(removeMock).when(removeMock).matching(any(Query.class));
+		when(removeMock.all()).thenReturn(DeleteResult.unacknowledged());
 		Method method = ReflectionUtils.findMethod(PersonRepository.class, "deleteAllByLastname", String.class);
 		MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context);
 
-		when(mongoOperationsMock.remove(any(Query.class), any(Class.class), anyString()))
-				.thenReturn(DeleteResult.unacknowledged());
-
-		assertThat(new DeleteExecution(mongoOperationsMock, queryMethod).execute(new Query())).isEqualTo(0L);
+		assertThat(new DeleteExecution(removeMock, queryMethod).execute(new Query())).isEqualTo(0L);
 	}
 
 	@Test // DATAMONGO-1997
 	void deleteExecutionWithEntityReturnTypeTriggersFindAndRemove() {
 
-		Method method = ReflectionUtils.findMethod(PersonRepository.class, "deleteByLastname", String.class);
-		MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context);
-
 		Person person = new Person();
+		doReturn(removeMock).when(removeMock).matching(any(Query.class));
+		when(removeMock.findAndRemove()).thenReturn(List.of(person));
 
-		when(mongoOperationsMock.findAndRemove(any(Query.class), any(Class.class), anyString())).thenReturn(person);
+		Method method = ReflectionUtils.findMethod(PersonRepository.class, "deleteByLastname", String.class);
+		MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context);
 
-		assertThat(new DeleteExecution(mongoOperationsMock, queryMethod).execute(new Query())).isEqualTo(person);
+		assertThat(new DeleteExecution(removeMock, queryMethod).execute(new Query())).isEqualTo(person);
 	}
 
 	interface PersonRepository extends Repository<Person, Long> {
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryMethodUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryMethodUnitTests.java
index 8f9824e14d..386d0fa4b5 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryMethodUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryMethodUnitTests.java
@@ -23,6 +23,7 @@
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.geo.Distance;
 import org.springframework.data.geo.GeoPage;
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQueryUnitTests.java
index e0b9b77099..07c10592d9 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQueryUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQueryUnitTests.java
@@ -45,8 +45,7 @@
 import org.springframework.data.projection.ProjectionFactory;
 import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
 import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
-import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
-import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.data.repository.query.ValueExpressionDelegate;
 
 /**
  * Unit tests for {@link PartTreeMongoQuery}.
@@ -206,8 +205,7 @@ private PartTreeMongoQuery createQueryForMethod(String methodName, Class<?>... p
 			MongoQueryMethod queryMethod = new MongoQueryMethod(method, new DefaultRepositoryMetadata(Repo.class), factory,
 					mappingContext);
 
-			return new PartTreeMongoQuery(queryMethod, mongoOperationsMock, new SpelExpressionParser(),
-					QueryMethodEvaluationContextProvider.DEFAULT);
+			return new PartTreeMongoQuery(queryMethod, mongoOperationsMock, ValueExpressionDelegate.create());
 		} catch (Exception e) {
 			throw new IllegalArgumentException(e.getMessage(), e);
 		}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecutionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecutionUnitTests.java
index 21d5dc71fb..1fbd60414a 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecutionUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecutionUnitTests.java
@@ -46,7 +46,7 @@
 import org.springframework.data.mongodb.repository.Person;
 import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.DeleteExecution;
 import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.GeoNearExecution;
-import org.springframework.data.util.ClassTypeInformation;
+import org.springframework.data.util.TypeInformation;
 import org.springframework.util.ClassUtils;
 
 import com.mongodb.client.result.DeleteResult;
@@ -71,10 +71,10 @@ public void geoNearExecutionShouldApplyQuerySettings() throws Exception {
 		Query query = new Query();
 		when(parameterAccessor.getGeoNearLocation()).thenReturn(new Point(1, 2));
 		when(parameterAccessor.getDistanceRange())
-				.thenReturn(Range.from(Bound.inclusive(new Distance(10))).to(Bound.inclusive(new Distance(15))));
+				.thenReturn(Range.from(Bound.inclusive(Distance.of(10))).to(Bound.inclusive(Distance.of(15))));
 		when(parameterAccessor.getPageable()).thenReturn(PageRequest.of(1, 10));
 
-		new GeoNearExecution(operations, parameterAccessor, ClassTypeInformation.fromReturnTypeOf(geoNear)).execute(query,
+		new GeoNearExecution(operations, parameterAccessor, TypeInformation.fromReturnTypeOf(geoNear)).execute(query,
 				Person.class, "person");
 
 		ArgumentCaptor<NearQuery> queryArgumentCaptor = ArgumentCaptor.forClass(NearQuery.class);
@@ -83,8 +83,8 @@ public void geoNearExecutionShouldApplyQuerySettings() throws Exception {
 		NearQuery nearQuery = queryArgumentCaptor.getValue();
 		assertThat(nearQuery.toDocument().get("near")).isEqualTo(Arrays.asList(1d, 2d));
 		assertThat(nearQuery.getSkip()).isEqualTo(10L);
-		assertThat(nearQuery.getMinDistance()).isEqualTo(new Distance(10));
-		assertThat(nearQuery.getMaxDistance()).isEqualTo(new Distance(15));
+		assertThat(nearQuery.getMinDistance()).isEqualTo(Distance.of(10));
+		assertThat(nearQuery.getMaxDistance()).isEqualTo(Distance.of(15));
 	}
 
 	@Test // DATAMONGO-1444
@@ -96,7 +96,7 @@ public void geoNearExecutionShouldApplyMinimalSettings() throws Exception {
 		when(parameterAccessor.getGeoNearLocation()).thenReturn(new Point(1, 2));
 		when(parameterAccessor.getDistanceRange()).thenReturn(Range.unbounded());
 
-		new GeoNearExecution(operations, parameterAccessor, ClassTypeInformation.fromReturnTypeOf(geoNear)).execute(query,
+		new GeoNearExecution(operations, parameterAccessor, TypeInformation.fromReturnTypeOf(geoNear)).execute(query,
 				Person.class, "person");
 
 		ArgumentCaptor<NearQuery> queryArgumentCaptor = ArgumentCaptor.forClass(NearQuery.class);
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethodUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethodUnitTests.java
index 82cd0a157c..14cbbc0394 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethodUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethodUnitTests.java
@@ -17,7 +17,6 @@
 
 import static org.assertj.core.api.Assertions.*;
 
-import org.springframework.data.mongodb.repository.query.MongoQueryMethodUnitTests.PersonRepository;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
@@ -27,6 +26,7 @@
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+
 import org.springframework.dao.InvalidDataAccessApiUsageException;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java
index c6047ce30d..b55ee77732 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java
@@ -27,6 +27,7 @@
 import java.util.List;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -34,6 +35,7 @@
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.reactivestreams.Publisher;
+
 import org.springframework.data.domain.Sort;
 import org.springframework.data.domain.Sort.Direction;
 import org.springframework.data.mongodb.core.ReactiveMongoOperations;
@@ -54,10 +56,8 @@
 import org.springframework.data.projection.ProjectionFactory;
 import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
 import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
-import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
+import org.springframework.data.repository.query.ValueExpressionDelegate;
 import org.springframework.data.repository.reactive.ReactiveCrudRepository;
-import org.springframework.expression.spel.standard.SpelExpressionParser;
-import org.springframework.lang.Nullable;
 import org.springframework.util.ClassUtils;
 
 import com.mongodb.ReadPreference;
@@ -71,8 +71,6 @@
 @ExtendWith(MockitoExtension.class)
 public class ReactiveStringBasedAggregationUnitTests {
 
-	SpelExpressionParser PARSER = new SpelExpressionParser();
-
 	@Mock ReactiveMongoOperations operations;
 	@Mock DbRefResolver dbRefResolver;
 	MongoConverter converter;
@@ -226,8 +224,7 @@ private ReactiveStringBasedAggregation createAggregationForMethod(String name, C
 		ProjectionFactory factory = new SpelAwareProxyProjectionFactory();
 		ReactiveMongoQueryMethod queryMethod = new ReactiveMongoQueryMethod(method,
 				new DefaultRepositoryMetadata(SampleRepository.class), factory, converter.getMappingContext());
-		return new ReactiveStringBasedAggregation(queryMethod, operations, PARSER,
-				ReactiveQueryMethodEvaluationContextProvider.DEFAULT);
+		return new ReactiveStringBasedAggregation(queryMethod, operations, ValueExpressionDelegate.create());
 	}
 
 	private List<Document> pipelineOf(AggregationInvocation invocation) {
@@ -242,27 +239,24 @@ private Class<?> inputTypeOf(AggregationInvocation invocation) {
 		return invocation.aggregation.getInputType();
 	}
 
-	@Nullable
-	private Collation collationOf(AggregationInvocation invocation) {
+	private @Nullable Collation collationOf(AggregationInvocation invocation) {
 		return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getCollation().orElse(null)
 				: null;
 	}
 
-	@Nullable
-	private Object hintOf(AggregationInvocation invocation) {
-		return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getHintObject().orElse(null)
+	private @Nullable Object hintOf(AggregationInvocation invocation) {
+		return invocation.aggregation.getOptions() != null
+				? invocation.aggregation.getOptions().getHintObject().orElse(null)
 				: null;
 	}
 
 	private Boolean skipResultsOf(AggregationInvocation invocation) {
-		return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().isSkipResults()
-				: false;
+		return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().isSkipResults() : false;
 	}
 
 	@Nullable
 	private ReadPreference readPreferenceOf(AggregationInvocation invocation) {
-		return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getReadPreference()
-				: null;
+		return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getReadPreference() : null;
 	}
 
 	private Class<?> targetTypeOf(AggregationInvocation invocation) {
@@ -284,7 +278,7 @@ private interface SampleRepository extends ReactiveCrudRepository<Person, Long>
 		@Aggregation(GROUP_BY_LASTNAME_STRING_WITH_SPEL_PARAMETER_PLACEHOLDER)
 		Mono<PersonAggregate> spelParameterReplacementAggregation(String arg0);
 
-		@Aggregation(pipeline = {RAW_GROUP_BY_LASTNAME_STRING, GROUP_BY_LASTNAME_STRING_WITH_SPEL_PARAMETER_PLACEHOLDER})
+		@Aggregation(pipeline = { RAW_GROUP_BY_LASTNAME_STRING, GROUP_BY_LASTNAME_STRING_WITH_SPEL_PARAMETER_PLACEHOLDER })
 		Mono<PersonAggregate> multiOperationPipeline(String arg0);
 
 		@Aggregation(pipeline = RAW_GROUP_BY_LASTNAME_STRING, collation = "de_AT")
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQueryUnitTests.java
index 72f9626a57..7358bf4ce6 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQueryUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQueryUnitTests.java
@@ -56,8 +56,6 @@
 import org.springframework.data.repository.Repository;
 import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
 import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor;
-import org.springframework.data.repository.query.ReactiveExtensionAwareQueryMethodEvaluationContextProvider;
-import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
 import org.springframework.data.repository.query.ValueExpressionDelegate;
 import org.springframework.data.spel.spi.EvaluationContextExtension;
 import org.springframework.data.spel.spi.ReactiveEvaluationContextExtension;
@@ -248,8 +246,8 @@ public void shouldSupportNonQuotedBinaryDataReplacement() throws Exception {
 		ReactiveStringBasedMongoQuery mongoQuery = createQueryForMethod("findByLastnameAsBinary", byte[].class);
 
 		org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor).block();
-		org.springframework.data.mongodb.core.query.Query reference = new BasicQuery(
-				"{'lastname' : { '$binary' : '" + Base64.getEncoder().encodeToString(binaryData) + "', '$type' : '" + 0 + "'}}");
+		org.springframework.data.mongodb.core.query.Query reference = new BasicQuery("{'lastname' : { '$binary' : '"
+				+ Base64.getEncoder().encodeToString(binaryData) + "', '$type' : '" + 0 + "'}}");
 
 		assertThat(query.getQueryObject().toJson()).isEqualTo(reference.getQueryObject().toJson());
 	}
@@ -266,16 +264,14 @@ void shouldConsiderReactiveSpelExtension() throws Exception {
 		assertThat(query.getQueryObject().toJson()).isEqualTo(reference.getQueryObject().toJson());
 	}
 
-	private ReactiveStringBasedMongoQuery createQueryForMethod(
-			String name, Class<?>... parameters)
-			throws Exception {
+	private ReactiveStringBasedMongoQuery createQueryForMethod(String name, Class<?>... parameters) throws Exception {
 
 		Method method = SampleRepository.class.getMethod(name, parameters);
 		ProjectionFactory factory = new SpelAwareProxyProjectionFactory();
 		ReactiveMongoQueryMethod queryMethod = new ReactiveMongoQueryMethod(method,
 				new DefaultRepositoryMetadata(SampleRepository.class), factory, converter.getMappingContext());
-		QueryMethodValueEvaluationContextAccessor accessor = new QueryMethodValueEvaluationContextAccessor(
-				environment, Collections.singletonList(ReactiveSpelExtension.INSTANCE));
+		QueryMethodValueEvaluationContextAccessor accessor = new QueryMethodValueEvaluationContextAccessor(environment,
+				Collections.singletonList(ReactiveSpelExtension.INSTANCE));
 		return new ReactiveStringBasedMongoQuery(queryMethod, operations, new ValueExpressionDelegate(accessor, PARSER));
 	}
 
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java
index 85a8650b26..827168007e 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java
@@ -27,6 +27,7 @@
 import java.util.stream.Stream;
 
 import org.bson.Document;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -35,6 +36,7 @@
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.mockito.junit.jupiter.MockitoSettings;
 import org.mockito.quality.Strictness;
+
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Pageable;
@@ -62,9 +64,7 @@
 import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
 import org.springframework.data.repository.Repository;
 import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
-import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
-import org.springframework.expression.spel.standard.SpelExpressionParser;
-import org.springframework.lang.Nullable;
+import org.springframework.data.repository.query.ValueExpressionDelegate;
 import org.springframework.util.ClassUtils;
 
 import com.mongodb.MongoClientSettings;
@@ -81,8 +81,6 @@
 @MockitoSettings(strictness = Strictness.LENIENT)
 public class StringBasedAggregationUnitTests {
 
-	private SpelExpressionParser PARSER = new SpelExpressionParser();
-
 	@Mock MongoOperations operations;
 	@Mock DbRefResolver dbRefResolver;
 	@Mock AggregationResults aggregationResults;
@@ -254,8 +252,7 @@ void aggregateRaisesErrorOnInvalidReturnType() {
 				factory, converter.getMappingContext());
 
 		assertThatExceptionOfType(InvalidMongoDbApiUsageException.class) //
-				.isThrownBy(() -> new StringBasedAggregation(queryMethod, operations, PARSER,
-						QueryMethodEvaluationContextProvider.DEFAULT)) //
+				.isThrownBy(() -> new StringBasedAggregation(queryMethod, operations, ValueExpressionDelegate.create())) //
 				.withMessageContaining("pageIsUnsupported") //
 				.withMessageContaining("Page");
 	}
@@ -311,7 +308,7 @@ private StringBasedAggregation createAggregationForMethod(String name, Class<?>.
 		ProjectionFactory factory = new SpelAwareProxyProjectionFactory();
 		MongoQueryMethod queryMethod = new MongoQueryMethod(method, new DefaultRepositoryMetadata(SampleRepository.class),
 				factory, converter.getMappingContext());
-		return new StringBasedAggregation(queryMethod, operations, PARSER, QueryMethodEvaluationContextProvider.DEFAULT);
+		return new StringBasedAggregation(queryMethod, operations, ValueExpressionDelegate.create());
 	}
 
 	private List<Document> pipelineOf(AggregationInvocation invocation) {
@@ -326,27 +323,24 @@ private Class<?> inputTypeOf(AggregationInvocation invocation) {
 		return invocation.aggregation.getInputType();
 	}
 
-	@Nullable
-	private Collation collationOf(AggregationInvocation invocation) {
+	private @Nullable Collation collationOf(AggregationInvocation invocation) {
 		return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getCollation().orElse(null)
 				: null;
 	}
 
-	@Nullable
-	private Object hintOf(AggregationInvocation invocation) {
-		return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getHintObject().orElse(null)
+	private @Nullable Object hintOf(AggregationInvocation invocation) {
+		return invocation.aggregation.getOptions() != null
+				? invocation.aggregation.getOptions().getHintObject().orElse(null)
 				: null;
 	}
 
 	private Boolean skipResultsOf(AggregationInvocation invocation) {
-		return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().isSkipResults()
-				: false;
+		return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().isSkipResults() : false;
 	}
 
 	@Nullable
 	private ReadPreference readPreferenceOf(AggregationInvocation invocation) {
-		return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getReadPreference()
-				: null;
+		return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getReadPreference() : null;
 	}
 
 	private Class<?> targetTypeOf(AggregationInvocation invocation) {
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java
index 1927378e80..91f23bb049 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java
@@ -18,11 +18,15 @@
 import java.util.Arrays;
 import java.util.Iterator;
 
+import org.jspecify.annotations.Nullable;
+
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.Range;
 import org.springframework.data.domain.Range.Bound;
+import org.springframework.data.domain.Score;
 import org.springframework.data.domain.ScrollPosition;
 import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Vector;
 import org.springframework.data.geo.Distance;
 import org.springframework.data.geo.Point;
 import org.springframework.data.mongodb.core.convert.MongoWriter;
@@ -30,7 +34,6 @@
 import org.springframework.data.mongodb.core.query.TextCriteria;
 import org.springframework.data.mongodb.core.query.UpdateDefinition;
 import org.springframework.data.repository.query.ParameterAccessor;
-import org.springframework.lang.Nullable;
 
 /**
  * Simple {@link ParameterAccessor} that returns the given parameters unfiltered.
@@ -73,6 +76,21 @@ public StubParameterAccessor(Object... values) {
 		}
 	}
 
+	@Override
+	public Vector getVector() {
+		return null;
+	}
+
+	@Override
+	public @org.jspecify.annotations.Nullable Score getScore() {
+		return null;
+	}
+
+	@Override
+	public @org.jspecify.annotations.Nullable Range<Score> getScoreRange() {
+		return null;
+	}
+
 	@Override
 	public ScrollPosition getScrollPosition() {
 		return null;
@@ -121,7 +139,7 @@ public Collation getCollation() {
 	 * @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getValues()
 	 */
 	@Override
-	public Object[] getValues() {
+	public Object @Nullable[] getValues() {
 		return this.values;
 	}
 
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregationUnitTests.java
new file mode 100644
index 0000000000..819bba5a48
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregationUnitTests.java
@@ -0,0 +1,109 @@
+/*
+ * 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.mongodb.repository.query;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.lang.reflect.Method;
+
+import org.bson.conversions.Bson;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import org.springframework.data.domain.Limit;
+import org.springframework.data.domain.Score;
+import org.springframework.data.domain.SearchResults;
+import org.springframework.data.domain.Vector;
+import org.springframework.data.mongodb.core.MongoOperations;
+import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
+import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
+import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
+import org.springframework.data.mongodb.repository.VectorSearch;
+import org.springframework.data.mongodb.repository.query.VectorSearchDelegate.QueryContainer;
+import org.springframework.data.projection.ProjectionFactory;
+import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
+import org.springframework.data.repository.query.ValueExpressionDelegate;
+
+/**
+ * Unit tests for {@link VectorSearchAggregation}.
+ *
+ * @author Mark Paluch
+ */
+class VectorSearchAggregationUnitTests {
+
+	MongoOperations operationsMock;
+	MongoMappingContext context;
+	MappingMongoConverter converter;
+
+	@BeforeEach
+	public void setUp() {
+
+		context = new MongoMappingContext();
+		converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, context);
+		operationsMock = Mockito.mock(MongoOperations.class);
+
+		when(operationsMock.getConverter()).thenReturn(converter);
+		when(operationsMock.execute(any())).thenReturn(Bson.DEFAULT_CODEC_REGISTRY);
+	}
+
+	@Test
+	void derivesPrefilter() throws Exception {
+
+		VectorSearchAggregation aggregation = aggregation(SampleRepository.class, "searchByCountryAndEmbeddingNear",
+				String.class, Vector.class, Score.class, Limit.class);
+
+		QueryContainer query = aggregation.createVectorSearchQuery(
+				aggregation.getQueryMethod().getResultProcessor(),
+				new MongoParametersParameterAccessor(aggregation.getQueryMethod(),
+						new Object[] { "de", Vector.of(1f), Score.of(1), Limit.unlimited() }),
+				Object.class);
+
+		assertThat(query.query().getQueryObject()).containsEntry("country", "de");
+	}
+
+	private VectorSearchAggregation aggregation(Class<?> repository, String name, Class<?>... parameters)
+			throws Exception {
+
+		Method method = repository.getMethod(name, parameters);
+		ProjectionFactory factory = new SpelAwareProxyProjectionFactory();
+		MongoQueryMethod queryMethod = new MongoQueryMethod(method, new DefaultRepositoryMetadata(repository), factory,
+				context);
+		return new VectorSearchAggregation(queryMethod, operationsMock, ValueExpressionDelegate.create());
+	}
+
+	interface SampleRepository extends CrudRepository<WithVectorFields, String> {
+
+		@VectorSearch(indexName = "cos-index")
+		SearchResults<WithVectorFields> searchByCountryAndEmbeddingNear(String country, Vector vector, Score similarity,
+				Limit limit);
+
+	}
+
+	static class WithVectorFields {
+
+		String id;
+		String country;
+		String description;
+
+		Vector embedding;
+
+	}
+
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegateUnitTests.java
new file mode 100644
index 0000000000..078c01eece
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegateUnitTests.java
@@ -0,0 +1,254 @@
+/*
+ * 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.mongodb.repository.query;
+
+import static org.mockito.Mockito.mock;
+import static org.springframework.data.mongodb.test.util.Assertions.assertThat;
+
+import java.lang.reflect.Method;
+import java.util.List;
+
+import org.bson.Document;
+import org.jspecify.annotations.Nullable;
+import org.junit.jupiter.api.Test;
+import org.springframework.data.domain.Limit;
+import org.springframework.data.domain.Score;
+import org.springframework.data.domain.SearchResults;
+import org.springframework.data.domain.Vector;
+import org.springframework.data.mapping.model.ValueExpressionEvaluator;
+import org.springframework.data.mongodb.core.aggregation.Aggregation;
+import org.springframework.data.mongodb.core.aggregation.AggregationPipeline;
+import org.springframework.data.mongodb.core.aggregation.VectorSearchOperation;
+import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
+import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
+import org.springframework.data.mongodb.core.mapping.Field;
+import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
+import org.springframework.data.mongodb.repository.VectorSearch;
+import org.springframework.data.mongodb.repository.query.VectorSearchDelegate.QueryContainer;
+import org.springframework.data.mongodb.util.aggregation.TestAggregationContext;
+import org.springframework.data.mongodb.util.json.ParameterBindingContext;
+import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec;
+import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
+import org.springframework.data.repository.Repository;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.core.support.AnnotationRepositoryMetadata;
+import org.springframework.data.repository.query.ValueExpressionDelegate;
+
+/**
+ * Unit tests for {@link VectorSearchDelegate}.
+ *
+ * @author Mark Paluch
+ * @author Christoph Strobl
+ */
+class VectorSearchDelegateUnitTests {
+
+	MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, new MongoMappingContext());
+
+	@Test
+	void shouldConsiderDerivedLimit() throws ReflectiveOperationException {
+
+		Method method = VectorSearchRepository.class.getMethod("searchTop10ByEmbeddingNear", Vector.class, Score.class);
+
+		MongoQueryMethod queryMethod = getMongoQueryMethod(method);
+		MongoParametersParameterAccessor accessor = getAccessor(queryMethod, Vector.of(1, 2), Score.of(1));
+
+		QueryContainer container = createQueryContainer(queryMethod, accessor);
+
+		assertThat(container.query().getLimit()).isEqualTo(10);
+		assertThat(numCandidates(container.pipeline())).isEqualTo(10 * 20);
+	}
+
+	@Test
+	void shouldNotSetNumCandidates() throws ReflectiveOperationException {
+
+		Method method = VectorSearchRepository.class.getMethod("searchTop10EnnByEmbeddingNear", Vector.class, Score.class);
+
+		MongoQueryMethod queryMethod = getMongoQueryMethod(method);
+		MongoParametersParameterAccessor accessor = getAccessor(queryMethod, Vector.of(1, 2), Score.of(1));
+
+		QueryContainer container = createQueryContainer(queryMethod, accessor);
+
+		assertThat(container.query().getLimit()).isEqualTo(10);
+		assertThat(numCandidates(container.pipeline())).isNull();
+	}
+
+	@Test
+	void shouldConsiderProvidedLimit() throws ReflectiveOperationException {
+
+		Method method = VectorSearchRepository.class.getMethod("searchTop10ByEmbeddingNear", Vector.class, Score.class,
+				Limit.class);
+
+		MongoQueryMethod queryMethod = getMongoQueryMethod(method);
+		MongoParametersParameterAccessor accessor = getAccessor(queryMethod, Vector.of(1, 2), Score.of(1), Limit.of(11));
+
+		QueryContainer container = createQueryContainer(queryMethod, accessor);
+
+		assertThat(container.query().getLimit()).isEqualTo(11);
+		assertThat(numCandidates(container.pipeline())).isEqualTo(11 * 20);
+	}
+
+	@Test
+	void considersDerivedQueryPart() throws ReflectiveOperationException {
+
+		Method method = VectorSearchRepository.class.getMethod("searchTop10ByFirstNameAndEmbeddingNear", String.class,
+				Vector.class, Score.class);
+
+		MongoQueryMethod queryMethod = getMongoQueryMethod(method);
+		MongoParametersParameterAccessor accessor = getAccessor(queryMethod, "spring", Vector.of(1, 2), Score.of(1));
+
+		QueryContainer container = createQueryContainer(queryMethod, accessor);
+
+		assertThat(vectorSearchStageOf(container.pipeline())).containsEntry("$vectorSearch.filter",
+				new Document("first_name", "spring"));
+	}
+
+	@Test
+	void considersDerivedQueryPartInDifferentOrder() throws ReflectiveOperationException {
+
+		Method method = VectorSearchRepository.class.getMethod("searchTop10ByEmbeddingNearAndFirstName", Vector.class,
+				Score.class, String.class);
+
+		MongoQueryMethod queryMethod = getMongoQueryMethod(method);
+		MongoParametersParameterAccessor accessor = getAccessor(queryMethod, Vector.of(1, 2), Score.of(1), "spring");
+
+		QueryContainer container = createQueryContainer(queryMethod, accessor);
+
+		assertThat(vectorSearchStageOf(container.pipeline())).containsEntry("$vectorSearch.filter",
+				new Document("first_name", "spring"));
+	}
+
+	@Test
+	void defaultSortsByScore() throws NoSuchMethodException {
+
+		Method method = VectorSearchRepository.class.getMethod("searchTop10ByEmbeddingNear", Vector.class, Score.class,
+				Limit.class);
+
+		MongoQueryMethod queryMethod = getMongoQueryMethod(method);
+		MongoParametersParameterAccessor accessor = getAccessor(queryMethod, Vector.of(1, 2), Score.of(1), Limit.of(10));
+
+		QueryContainer container = createQueryContainer(queryMethod, accessor);
+
+		List<Document> stages = container.pipeline().lastOperation()
+				.toPipelineStages(TestAggregationContext.contextFor(WithVector.class));
+
+		assertThat(stages).containsExactly(new Document("$sort", new Document("__score__", -1)));
+	}
+
+	@Test
+	void usesDerivedSort() throws NoSuchMethodException {
+
+		Method method = VectorSearchRepository.class.getMethod("searchByEmbeddingNearOrderByFirstName", Vector.class,
+				Score.class, Limit.class);
+
+		MongoQueryMethod queryMethod = getMongoQueryMethod(method);
+		MongoParametersParameterAccessor accessor = getAccessor(queryMethod, Vector.of(1, 2), Score.of(1), Limit.of(11));
+
+		QueryContainer container = createQueryContainer(queryMethod, accessor);
+		AggregationPipeline aggregationPipeline = container.pipeline();
+
+		List<Document> stages = aggregationPipeline.lastOperation()
+				.toPipelineStages(TestAggregationContext.contextFor(WithVector.class));
+
+		assertThat(stages).containsExactly(new Document("$sort", new Document("first_name", 1).append("__score__", -1)));
+	}
+
+	Document vectorSearchStageOf(AggregationPipeline pipeline) {
+		return pipeline.firstOperation().toPipelineStages(TestAggregationContext.contextFor(WithVector.class)).get(0);
+	}
+
+	private QueryContainer createQueryContainer(MongoQueryMethod queryMethod, MongoParametersParameterAccessor accessor) {
+
+		VectorSearchDelegate delegate = new VectorSearchDelegate(queryMethod, converter, ValueExpressionDelegate.create());
+
+		return delegate.createQuery(mock(ValueExpressionEvaluator.class), queryMethod.getResultProcessor(), accessor, null,
+				new ParameterBindingDocumentCodec(), mock(ParameterBindingContext.class));
+	}
+
+	private MongoQueryMethod getMongoQueryMethod(Method method) {
+		RepositoryMetadata metadata = AnnotationRepositoryMetadata.getMetadata(method.getDeclaringClass());
+		return new MongoQueryMethod(method, metadata, new SpelAwareProxyProjectionFactory(), converter.getMappingContext());
+	}
+
+	private static MongoParametersParameterAccessor getAccessor(MongoQueryMethod queryMethod, Object... values) {
+		return new MongoParametersParameterAccessor(queryMethod, values);
+	}
+
+	@Nullable
+	private static Integer numCandidates(AggregationPipeline pipeline) {
+
+		Document $vectorSearch = pipeline.firstOperation().toPipelineStages(Aggregation.DEFAULT_CONTEXT).get(0);
+		if ($vectorSearch.containsKey("$vectorSearch")) {
+			Object value = $vectorSearch.get("$vectorSearch", Document.class).get("numCandidates");
+			return value instanceof Number i ? i.intValue() : null;
+		}
+		return null;
+	}
+
+	interface VectorSearchRepository extends Repository<WithVector, String> {
+
+		@VectorSearch(indexName = "cos-index", searchType = VectorSearchOperation.SearchType.ANN)
+		SearchResults<WithVector> searchTop10ByEmbeddingNear(Vector vector, Score similarity);
+
+		@VectorSearch(indexName = "cos-index", searchType = VectorSearchOperation.SearchType.ANN)
+		SearchResults<WithVector> searchTop10ByFirstNameAndEmbeddingNear(String firstName, Vector vector, Score similarity);
+
+		@VectorSearch(indexName = "cos-index", searchType = VectorSearchOperation.SearchType.ANN)
+		SearchResults<WithVector> searchTop10ByEmbeddingNearAndFirstName(Vector vector, Score similarity, String firstname);
+
+		@VectorSearch(indexName = "cos-index", searchType = VectorSearchOperation.SearchType.ENN)
+		SearchResults<WithVector> searchTop10EnnByEmbeddingNear(Vector vector, Score similarity);
+
+		@VectorSearch(indexName = "cos-index", searchType = VectorSearchOperation.SearchType.ANN)
+		SearchResults<WithVector> searchTop10ByEmbeddingNear(Vector vector, Score similarity, Limit limit);
+
+		@VectorSearch(indexName = "cos-index", searchType = VectorSearchOperation.SearchType.ANN)
+		SearchResults<WithVector> searchByEmbeddingNearOrderByFirstName(Vector vector, Score similarity, Limit limit);
+
+	}
+
+	static class WithVector {
+
+		Vector embedding;
+
+		String lastName;
+
+		@Field("first_name") String firstName;
+
+		public Vector getEmbedding() {
+			return embedding;
+		}
+
+		public void setEmbedding(Vector embedding) {
+			this.embedding = embedding;
+		}
+
+		public String getLastName() {
+			return lastName;
+		}
+
+		public void setLastName(String lastName) {
+			this.lastName = lastName;
+		}
+
+		public String getFirstName() {
+			return firstName;
+		}
+
+		public void setFirstName(String firstName) {
+			this.firstName = firstName;
+		}
+	}
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryUnitTests.java
index c40f24dacb..3d0e468155 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryUnitTests.java
@@ -31,9 +31,7 @@
 import org.mockito.junit.jupiter.MockitoSettings;
 import org.mockito.quality.Strictness;
 
-import org.springframework.data.mapping.context.MappingContext;
 import org.springframework.data.mongodb.core.MongoOperations;
-import org.springframework.data.mongodb.core.MongoTemplate;
 import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
 import org.springframework.data.mongodb.core.convert.MongoConverter;
 import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
@@ -41,9 +39,8 @@
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mongodb.repository.Person;
 import org.springframework.data.mongodb.repository.ReadPreference;
-import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
 import org.springframework.data.repository.ListCrudRepository;
-import org.springframework.data.repository.Repository;
+import org.springframework.data.repository.core.EntityInformation;
 
 /**
  * Unit test for {@link MongoRepositoryFactory}.
@@ -54,27 +51,27 @@
  */
 @ExtendWith(MockitoExtension.class)
 @MockitoSettings(strictness = Strictness.LENIENT)
-public class MongoRepositoryFactoryUnitTests {
+class MongoRepositoryFactoryUnitTests {
 
 	@Mock MongoOperations template;
 
-	MongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, new MongoMappingContext());
+	private MongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, new MongoMappingContext());
 
 	@BeforeEach
-	public void setUp() {
+	void setUp() {
 		when(template.getConverter()).thenReturn(converter);
 	}
 
 	@Test
-	public void usesMappingMongoEntityInformationIfMappingContextSet() {
+	void usesMappingMongoEntityInformationIfMappingContextSet() {
 
 		MongoRepositoryFactory factory = new MongoRepositoryFactory(template);
-		MongoEntityInformation<Person, Serializable> entityInformation = factory.getEntityInformation(Person.class);
+		EntityInformation<Person, Serializable> entityInformation = factory.getEntityInformation(Person.class);
 		assertThat(entityInformation instanceof MappingMongoEntityInformation).isTrue();
 	}
 
 	@Test // DATAMONGO-385
-	public void createsRepositoryWithIdTypeLong() {
+	void createsRepositoryWithIdTypeLong() {
 
 		MongoRepositoryFactory factory = new MongoRepositoryFactory(template);
 		MyPersonRepository repository = factory.getRepository(MyPersonRepository.class);
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFragmentsContributorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFragmentsContributorUnitTests.java
new file mode 100644
index 0000000000..564115fed0
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFragmentsContributorUnitTests.java
@@ -0,0 +1,93 @@
+/*
+ * 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.mongodb.repository.support;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Iterator;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.data.mongodb.core.MongoOperations;
+import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
+import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
+import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
+import org.springframework.data.mongodb.repository.User;
+import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
+import org.springframework.data.querydsl.QuerydslPredicateExecutor;
+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;
+import org.springframework.data.repository.core.support.RepositoryFragment;
+
+/**
+ * Unit tests for {@link MongoRepositoryFragmentsContributor}.
+ *
+ * @author Mark Paluch
+ */
+class MongoRepositoryFragmentsContributorUnitTests {
+
+	@Test // GH-4964
+	void composedContributorShouldCreateFragments() {
+
+		MongoMappingContext mappingContext = new MongoMappingContext();
+		MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext);
+		MongoOperations operations = mock(MongoOperations.class);
+		when(operations.getConverter()).thenReturn(converter);
+
+		MongoRepositoryFragmentsContributor contributor = MongoRepositoryFragmentsContributor.DEFAULT
+				.andThen(MyMongoRepositoryFragmentsContributor.INSTANCE);
+
+		RepositoryComposition.RepositoryFragments fragments = contributor.contribute(
+				AbstractRepositoryMetadata.getMetadata(QuerydslUserRepository.class),
+				new MappingMongoEntityInformation<>(mappingContext.getPersistentEntity(User.class)), operations);
+
+		assertThat(fragments).hasSize(2);
+
+		Iterator<RepositoryFragment<?>> iterator = fragments.iterator();
+
+		RepositoryFragment<?> querydsl = iterator.next();
+		assertThat(querydsl.getImplementationClass()).contains(QuerydslMongoPredicateExecutor.class);
+
+		RepositoryFragment<?> additional = iterator.next();
+		assertThat(additional.getImplementationClass()).contains(MyFragment.class);
+	}
+
+	enum MyMongoRepositoryFragmentsContributor implements MongoRepositoryFragmentsContributor {
+
+		INSTANCE;
+
+		@Override
+		public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata,
+				MongoEntityInformation<?, ?> entityInformation, MongoOperations operations) {
+			return RepositoryComposition.RepositoryFragments.just(new MyFragment());
+		}
+
+		@Override
+		public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) {
+			return RepositoryComposition.RepositoryFragments.just(new MyFragment());
+		}
+	}
+
+	static class MyFragment {
+
+	}
+
+	interface QuerydslUserRepository extends Repository<User, Long>, QuerydslPredicateExecutor<User> {}
+
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java
index 7d9024e2fb..5f0800aba6 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java
@@ -75,7 +75,8 @@ public class QuerydslMongoPredicateExecutorIntegrationTests {
 	public void setup() {
 
 		MongoRepositoryFactory factory = new MongoRepositoryFactory(operations);
-		MongoEntityInformation<Person, String> entityInformation = factory.getEntityInformation(Person.class);
+		MongoEntityInformation<Person, String> entityInformation = factory
+				.getEntityInformation(Person.class);
 		repository = new QuerydslMongoPredicateExecutor<>(entityInformation, operations);
 
 		operations.dropCollection(Person.class);
@@ -246,7 +247,8 @@ protected MongoDatabase doGetDatabase() {
 		};
 
 		MongoRepositoryFactory factory = new MongoRepositoryFactory(ops);
-		MongoEntityInformation<Person, String> entityInformation = factory.getEntityInformation(Person.class);
+		MongoEntityInformation<Person, String> entityInformation = factory
+				.getEntityInformation(Person.class);
 		repository = new QuerydslMongoPredicateExecutor<>(entityInformation, ops);
 
 		repository.findOne(person.firstname.contains("batman"));
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFragmentsContributorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFragmentsContributorUnitTests.java
new file mode 100644
index 0000000000..065ff27654
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFragmentsContributorUnitTests.java
@@ -0,0 +1,93 @@
+/*
+ * 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.mongodb.repository.support;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Iterator;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.data.mongodb.core.ReactiveMongoOperations;
+import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
+import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
+import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
+import org.springframework.data.mongodb.repository.User;
+import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
+import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor;
+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;
+import org.springframework.data.repository.core.support.RepositoryFragment;
+
+/**
+ * Unit tests for {@link ReactiveMongoRepositoryFragmentsContributor}.
+ *
+ * @author Mark Paluch
+ */
+class ReactiveMongoRepositoryFragmentsContributorUnitTests {
+
+	@Test // GH-4964
+	void composedContributorShouldCreateFragments() {
+
+		MongoMappingContext mappingContext = new MongoMappingContext();
+		MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext);
+		ReactiveMongoOperations operations = mock(ReactiveMongoOperations.class);
+		when(operations.getConverter()).thenReturn(converter);
+
+		ReactiveMongoRepositoryFragmentsContributor contributor = ReactiveMongoRepositoryFragmentsContributor.DEFAULT
+				.andThen(MyMongoRepositoryFragmentsContributor.INSTANCE);
+
+		RepositoryComposition.RepositoryFragments fragments = contributor.contribute(
+				AbstractRepositoryMetadata.getMetadata(QuerydslUserRepository.class),
+				new MappingMongoEntityInformation<>(mappingContext.getPersistentEntity(User.class)), operations);
+
+		assertThat(fragments).hasSize(2);
+
+		Iterator<RepositoryFragment<?>> iterator = fragments.iterator();
+
+		RepositoryFragment<?> querydsl = iterator.next();
+		assertThat(querydsl.getImplementationClass()).contains(ReactiveQuerydslMongoPredicateExecutor.class);
+
+		RepositoryFragment<?> additional = iterator.next();
+		assertThat(additional.getImplementationClass()).contains(MyFragment.class);
+	}
+
+	enum MyMongoRepositoryFragmentsContributor implements ReactiveMongoRepositoryFragmentsContributor {
+
+		INSTANCE;
+
+		@Override
+		public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata,
+				MongoEntityInformation<?, ?> entityInformation, ReactiveMongoOperations operations) {
+			return RepositoryComposition.RepositoryFragments.just(new MyFragment());
+		}
+
+		@Override
+		public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) {
+			return RepositoryComposition.RepositoryFragments.just(new MyFragment());
+		}
+	}
+
+	static class MyFragment {
+
+	}
+
+	interface QuerydslUserRepository extends Repository<User, Long>, ReactiveQuerydslPredicateExecutor<User> {}
+
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorTests.java
index 807b7aec22..c807a1bcbd 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorTests.java
@@ -111,7 +111,8 @@ public static void cleanDb() {
 	public void setup() {
 
 		ReactiveMongoRepositoryFactory factory = new ReactiveMongoRepositoryFactory(operations);
-		MongoEntityInformation<Person, String> entityInformation = factory.getEntityInformation(Person.class);
+		MongoEntityInformation<Person, String> entityInformation = factory
+				.getEntityInformation(Person.class);
 		repository = new ReactiveQuerydslMongoPredicateExecutor<>(entityInformation, operations);
 
 		dave = new Person("Dave", "Matthews", 42);
@@ -326,7 +327,8 @@ protected Mono<MongoDatabase> doGetDatabase() {
 		};
 
 		ReactiveMongoRepositoryFactory factory = new ReactiveMongoRepositoryFactory(ops);
-		MongoEntityInformation<Person, String> entityInformation = factory.getEntityInformation(Person.class);
+		MongoEntityInformation<Person, String> entityInformation = factory
+				.getEntityInformation(Person.class);
 		repository = new ReactiveQuerydslMongoPredicateExecutor<>(entityInformation, ops);
 
 		repository.findOne(person.firstname.contains("batman")) //
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/CleanMongoDBTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/CleanMongoDBTests.java
index f2fd993ef8..17a045b7a1 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/CleanMongoDBTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/CleanMongoDBTests.java
@@ -32,8 +32,8 @@
 import org.mockito.junit.jupiter.MockitoSettings;
 import org.mockito.quality.Strictness;
 import org.springframework.data.mongodb.test.util.CleanMongoDB.Struct;
-import org.springframework.data.mongodb.util.MongoCompatibilityAdapter;
 
+import com.mongodb.client.ListCollectionNamesIterable;
 import com.mongodb.client.ListDatabasesIterable;
 import com.mongodb.client.MongoClient;
 import com.mongodb.client.MongoCollection;
@@ -74,11 +74,11 @@ void setUp() throws ClassNotFoundException {
 		when(mongoClientMock.getDatabase(eq("db2"))).thenReturn(db2mock);
 
 		// collections have to exist
-		MongoIterable<String> collectionIterable = mock(MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(db1mock).collectionNameIterableType());
+		MongoIterable<String> collectionIterable = mock(ListCollectionNamesIterable.class);
 		when(collectionIterable.into(any(Collection.class))).thenReturn(Arrays.asList("db1collection1", "db1collection2"));
 		doReturn(collectionIterable).when(db1mock).listCollectionNames();
 
-		MongoIterable<String> collectionIterable2 = mock(MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(db2mock).collectionNameIterableType());
+		MongoIterable<String> collectionIterable2 = mock(ListCollectionNamesIterable.class);
 		when(collectionIterable2.into(any(Collection.class))).thenReturn(Collections.singletonList("db2collection1"));
 		doReturn(collectionIterable2).when(db2mock).listCollectionNames();
 
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MappingContextConfigurer.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MappingContextConfigurer.java
index 15a0538600..eda1e501a0 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MappingContextConfigurer.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MappingContextConfigurer.java
@@ -20,7 +20,7 @@
 import java.util.HashSet;
 import java.util.Set;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /**
  * Utility to configure {@link org.springframework.data.mongodb.core.mapping.MongoMappingContext} properties.
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplate.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplate.java
index 40948a0e22..771c17c4a9 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplate.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplate.java
@@ -23,12 +23,12 @@
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
+import com.mongodb.client.MongoClients;
 import org.bson.Document;
 import org.springframework.context.ApplicationContext;
 import org.springframework.data.mapping.callback.EntityCallbacks;
 import org.springframework.data.mapping.context.PersistentEntities;
 import org.springframework.data.mongodb.core.MongoTemplate;
-import org.springframework.data.mongodb.util.MongoCompatibilityAdapter;
 import org.testcontainers.shaded.org.awaitility.Awaitility;
 
 import com.mongodb.MongoWriteException;
@@ -45,6 +45,14 @@ public class MongoTestTemplate extends MongoTemplate {
 
 	private final MongoTestTemplateConfiguration cfg;
 
+	public MongoTestTemplate() {
+		this("test");
+	}
+
+	public MongoTestTemplate(String databaseName) {
+		this(MongoClients.create(), databaseName);
+	}
+
 	public MongoTestTemplate(MongoClient client, String database, Class<?>... initialEntities) {
 		this(cfg -> {
 			cfg.configureDatabaseFactory(it -> {
@@ -96,7 +104,7 @@ public void flush() {
 	}
 
 	public void flushDatabase() {
-		flush(MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(getDb()).listCollectionNames());
+		flush(getDb().listCollectionNames());
 	}
 
 	public void flush(Iterable<String> collections) {
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplateConfiguration.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplateConfiguration.java
index 09149c02ef..8300690ccd 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplateConfiguration.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplateConfiguration.java
@@ -20,6 +20,7 @@
 import java.util.function.Consumer;
 import java.util.function.Function;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.beans.BeansException;
 import org.springframework.beans.factory.ObjectFactory;
 import org.springframework.context.ApplicationContext;
@@ -39,7 +40,6 @@
 import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
 import org.springframework.data.mongodb.core.mapping.event.AuditingEntityCallback;
 import org.springframework.data.mongodb.core.mapping.event.MongoMappingEvent;
-import org.springframework.lang.Nullable;
 
 /**
  * @author Christoph Strobl
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapterUnitTests.java
deleted file mode 100644
index ab8e17a469..0000000000
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapterUnitTests.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright 2024-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.mongodb.util;
-
-import static org.assertj.core.api.Assertions.*;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.data.mongodb.test.util.ExcludeReactiveClientFromClassPath;
-import org.springframework.data.mongodb.test.util.ExcludeSyncClientFromClassPath;
-import org.springframework.util.ClassUtils;
-
-/**
- * @author Christoph Strobl
- */
-class MongoCompatibilityAdapterUnitTests {
-
-	@Test // GH-4578
-	@ExcludeReactiveClientFromClassPath
-	void returnsListCollectionNameIterableTypeCorrectly() {
-
-		String expectedType = MongoClientVersion.isVersion5orNewer() ? "ListCollectionNamesIterable" : "MongoIterable";
-		assertThat(MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(null).collectionNameIterableType())
-				.satisfies(type -> assertThat(ClassUtils.getShortName(type)).isEqualTo(expectedType));
-
-	}
-
-	@Test // GH-4578
-	@ExcludeSyncClientFromClassPath
-	void returnsListCollectionNamePublisherTypeCorrectly() {
-
-		String expectedType = MongoClientVersion.isVersion5orNewer() ? "ListCollectionNamesPublisher" : "Publisher";
-		assertThat(MongoCompatibilityAdapter.reactiveMongoDatabaseAdapter().forDb(null).collectionNamePublisherType())
-				.satisfies(type -> assertThat(ClassUtils.getShortName(type)).isEqualTo(expectedType));
-
-	}
-}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/SpringJsonWriterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/SpringJsonWriterUnitTests.java
new file mode 100644
index 0000000000..878623944c
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/SpringJsonWriterUnitTests.java
@@ -0,0 +1,161 @@
+/*
+ * 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.mongodb.util;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.bson.BsonRegularExpression;
+import org.bson.BsonTimestamp;
+import org.bson.types.Decimal128;
+import org.bson.types.ObjectId;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link SpringJsonWriter}.
+ *
+ * @author Christoph Strobl
+ * @since 5.0
+ */
+class SpringJsonWriterUnitTests {
+
+	StringBuffer buffer;
+	SpringJsonWriter writer;
+
+	@BeforeEach
+	void beforeEach() {
+		buffer = new StringBuffer();
+		writer = new SpringJsonWriter(buffer);
+	}
+
+	@Test
+	void writeDocumentWithSingleEntry() {
+
+		writer.writeStartDocument();
+		writer.writeString("key", "value");
+		writer.writeEndDocument();
+
+		assertThat(buffer).isEqualToIgnoringWhitespace("{'key':'value'}");
+	}
+
+	@Test
+	void writeDocumentWithMultipleEntries() {
+
+		writer.writeStartDocument();
+		writer.writeString("key-1", "v1");
+		writer.writeString("key-2", "v2");
+		writer.writeEndDocument();
+
+		assertThat(buffer).isEqualToIgnoringWhitespace("{'key-1':'v1','key-2':'v2'}");
+	}
+
+	@Test
+	void writeInt32() {
+
+		writer.writeInt32("int32", 32);
+
+		assertThat(buffer).isEqualToIgnoringWhitespace("'int32':{'$numberInt':'32'}");
+	}
+
+	@Test
+	void writeInt64() {
+
+		writer.writeInt64("int64", 64);
+
+		assertThat(buffer).isEqualToIgnoringWhitespace("'int64':{'$numberLong':'64'}");
+	}
+
+	@Test
+	void writeDouble() {
+
+		writer.writeDouble("double", 42.24D);
+
+		assertThat(buffer).isEqualToIgnoringWhitespace("'double':{'$numberDouble':'42.24'}");
+	}
+
+	@Test
+	void writeDecimal128() {
+
+		writer.writeDecimal128("decimal128", new Decimal128(128L));
+
+		assertThat(buffer).isEqualToIgnoringWhitespace("'decimal128':{'$numberDecimal':'128'}");
+	}
+
+	@Test
+	void writeObjectId() {
+
+		ObjectId objectId = new ObjectId();
+		writer.writeObjectId("_id", objectId);
+
+		assertThat(buffer).isEqualToIgnoringWhitespace("'_id':{'$oid':'%s'}".formatted(objectId.toHexString()));
+	}
+
+	@Test
+	void writeRegex() {
+
+		String pattern = "^H";
+		writer.writeRegularExpression("name", new BsonRegularExpression(pattern));
+
+		assertThat(buffer).isEqualToIgnoringWhitespace("'name':{'$regex':/%s/}".formatted(pattern));
+	}
+
+	@Test
+	void writeRegexWithOptions() {
+
+		String pattern = "^H";
+		writer.writeRegularExpression("name", new BsonRegularExpression(pattern, "i"));
+
+		assertThat(buffer).isEqualToIgnoringWhitespace("'name':{'$regex':/%s/,'$options':'%s'}".formatted(pattern, "i"));
+	}
+
+	@Test
+	void writeTimestamp() {
+
+		writer.writeTimestamp("ts", new BsonTimestamp(1234, 567));
+
+		assertThat(buffer).isEqualToIgnoringWhitespace("'ts':{'$timestamp':{'t':1234,'i':567}}");
+	}
+
+	@Test
+	void writeUndefined() {
+
+		writer.writeUndefined("nope");
+
+		assertThat(buffer).isEqualToIgnoringWhitespace("'nope':{'$undefined':true}");
+	}
+
+	@Test
+	void writeArrayWithSingleEntry() {
+
+		writer.writeStartArray();
+		writer.writeInt32(42);
+		writer.writeEndArray();
+
+		assertThat(buffer).isEqualToIgnoringNewLines("[{'$numberInt':'42'}]");
+	}
+
+	@Test
+	void writeArrayWithMultipleEntries() {
+
+		writer.writeStartArray();
+		writer.writeInt32(42);
+		writer.writeInt64(24);
+		writer.writeEndArray();
+
+		assertThat(buffer).isEqualToIgnoringNewLines("[{'$numberInt':'42'},{'$numberLong':'24'}]");
+	}
+
+}
diff --git a/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/ReactiveFindOperationExtensionsTests.kt b/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/ReactiveFindOperationExtensionsTests.kt
index cbb7ae46f3..99d57002e4 100644
--- a/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/ReactiveFindOperationExtensionsTests.kt
+++ b/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/ReactiveFindOperationExtensionsTests.kt
@@ -270,9 +270,9 @@ class ReactiveFindOperationExtensionsTests {
 	fun terminatingFindNearAllAsFlow() {
 
 		val spec = mockk<ReactiveFindOperation.TerminatingFindNear<String>>()
-		val foo = GeoResult("foo", Distance(0.0))
-		val bar = GeoResult("bar", Distance(0.0))
-		val baz = GeoResult("baz", Distance(0.0))
+		val foo = GeoResult("foo", Distance.of(0.0))
+		val bar = GeoResult("bar", Distance.of(0.0))
+		val baz = GeoResult("baz", Distance.of(0.0))
 		every { spec.all() } returns Flux.just(foo, bar, baz)
 
 		runBlocking {
diff --git a/spring-data-mongodb/src/test/resources/logback.xml b/spring-data-mongodb/src/test/resources/logback.xml
index 64550c957c..55e4309a36 100644
--- a/spring-data-mongodb/src/test/resources/logback.xml
+++ b/spring-data-mongodb/src/test/resources/logback.xml
@@ -19,6 +19,9 @@
 	</logger>
 	<logger name="org.springframework.data.mongodb.test.util" level="info"/>
 
+	<!-- AOT Code Generation -->
+	<logger name="org.springframework.data.repository.aot.generate.RepositoryContributor" level="warn" />
+
 	<root level="error">
 		<appender-ref ref="console" />
 	</root>
diff --git a/spring-data-mongodb/src/test/resources/server-jmx.xml b/spring-data-mongodb/src/test/resources/server-jmx.xml
deleted file mode 100644
index 54f985f4cb..0000000000
--- a/spring-data-mongodb/src/test/resources/server-jmx.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<beans xmlns="http://www.springframework.org/schema/beans"
-	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-	xmlns:p="http://www.springframework.org/schema/p"
-	xmlns:mongo="http://www.springframework.org/schema/data/mongo"
-	xmlns:context="http://www.springframework.org/schema/context"
-	xsi:schemaLocation="http://www.springframework.org/schema/data/mongo https://www.springframework.org/schema/data/mongo/spring-mongo.xsd
-		http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
-		http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
-
-  <mongo:jmx/>
-
-  <context:mbean-export/>
-
-  <bean id="registry" class="org.springframework.remoting.rmi.RmiRegistryFactoryBean"
-        p:port="1099"/>
-
-  <!-- Expose JMX over RMI -->
-  <bean id="serverConnector" class="org.springframework.jmx.support.ConnectorServerFactoryBean" depends-on="registry"
-        p:objectName="connector:name=rmi"
-        p:serviceUrl="service:jmx:rmi://localhost/jndi/rmi://localhost:1099/myconnector"/>
-
-</beans>
\ No newline at end of file
diff --git a/src/main/antora/antora-playbook.yml b/src/main/antora/antora-playbook.yml
index 9f842fe401..497f39ee04 100644
--- a/src/main/antora/antora-playbook.yml
+++ b/src/main/antora/antora-playbook.yml
@@ -17,7 +17,7 @@ content:
     - url: https://github.com/spring-projects/spring-data-commons
       # Refname matching:
       # https://docs.antora.org/antora/latest/playbook/content-refname-matching/
-      branches: [ main, 3.3.x, 3.2.x]
+      branches: [ main, 4.0.x ]
       start_path: src/main/antora
 asciidoc:
   attributes:
diff --git a/src/main/antora/modules/ROOT/nav.adoc b/src/main/antora/modules/ROOT/nav.adoc
index 221f47c011..6f2d1e2847 100644
--- a/src/main/antora/modules/ROOT/nav.adoc
+++ b/src/main/antora/modules/ROOT/nav.adoc
@@ -45,6 +45,7 @@
 ** xref:repositories/create-instances.adoc[]
 ** xref:repositories/query-methods-details.adoc[]
 ** xref:mongodb/repositories/query-methods.adoc[]
+** xref:mongodb/repositories/vector-search.adoc[]
 ** xref:mongodb/repositories/modifying-methods.adoc[]
 ** xref:repositories/projections.adoc[]
 ** xref:repositories/custom-implementations.adoc[]
@@ -53,6 +54,7 @@
 ** xref:mongodb/repositories/cdi-integration.adoc[]
 ** xref:repositories/query-keywords-reference.adoc[]
 ** xref:repositories/query-return-types-reference.adoc[]
+** xref:mongodb/aot.adoc[]
 
 // Observability
 * xref:observability/observability.adoc[]
diff --git a/src/main/antora/modules/ROOT/pages/mongodb/aot.adoc b/src/main/antora/modules/ROOT/pages/mongodb/aot.adoc
new file mode 100644
index 0000000000..16dd2f9ca0
--- /dev/null
+++ b/src/main/antora/modules/ROOT/pages/mongodb/aot.adoc
@@ -0,0 +1,85 @@
+= Ahead of Time Optimizations
+
+This chapter covers Spring Data's Ahead of Time (AOT) optimizations that build upon {spring-framework-docs}/core/aot.html[Spring's Ahead of Time Optimizations].
+
+[[aot.hints]]
+== Runtime Hints
+
+Running an application as a native image requires additional information compared to a regular JVM runtime.
+Spring Data contributes {spring-framework-docs}/core/aot.html#aot.hints[Runtime Hints] during AOT processing for native image usage.
+These are in particular hints for:
+
+* Auditing
+* `ManagedTypes` to capture the outcome of class-path scans
+* Repositories
+** Reflection hints for entities, return types, and Spring Data annotations
+** Repository fragments
+** Querydsl `Q` classes
+** Kotlin Coroutine support
+* Web support (Jackson Hints for `PagedModel`)
+
+[[aot.repositories]]
+== Ahead of Time Repositories
+
+AOT Repositories are an extension to AOT processing by pre-generating eligible query method implementations.
+Query methods are opaque to developers regarding their underlying queries being executed in a query method call.
+AOT repositories contribute query method implementations based on derived or annotated queries, updates or aggregations that are known at build-time.
+This optimization moves query method processing from runtime to build-time, which can lead to a significant bootstrap performance improvement as query methods do not need to be analyzed reflectively upon each application start.
+
+The resulting AOT repository fragment follows the naming scheme of `<Repository FQCN>Impl__Aot` and is placed in the same package as the repository interface.
+You can find all queries in their MQL form for generated repository query methods.
+
+[TIP]
+====
+`spring.aot.repositories.enabled` property needs to be set to `true` for repository fragment code generation.
+====
+
+[NOTE]
+====
+Consider AOT repository classes an internal optimization.
+Do not use them directly in your code as generation and implementation details may change in future releases.
+====
+
+=== Running with AOT Repositories
+
+AOT is a mandatory step to transform a Spring application to a native executable, so it is automatically enabled when running in this mode.
+It is also possible to use those optimizations on the JVM by setting the `spring.aot.enabled` and `spring.aot.repositories.enabled` System properties to `true`.
+
+AOT repositories contribute configuration changes to the actual repository bean registration to register the generated repository fragment.
+
+[NOTE]
+====
+When AOT optimizations are included, some decisions that have been taken at build-time are hard-coded in the application setup.
+For instance, profiles that have been enabled at build-time are automatically enabled at runtime as well.
+Also, the Spring Data module implementing a repository is fixed.
+Changing the implementation requires AOT re-processing.
+====
+
+=== Eligible Methods in Data MongoDB
+
+AOT repositories filter methods that are eligible for AOT processing.
+These are typically all query methods that are not backed by an xref:repositories/custom-implementations.adoc[implementation fragment].
+
+**Supported Features**
+
+* Derived `find`, `count`, `exists` and `delete` methods
+* Query methods annotated with `@Query` (excluding those containing SpEL)
+* Methods annotated with `@Aggregation`
+* Methods using `@Update`
+* `@Hint`, `@Meta`, and `@ReadPreference` support
+* `Page`, `Slice`, and `Optional` return types
+* DTO Projections
+
+**Limitations**
+
+* `@Meta.allowDiskUse` and `flags` are not evaluated.
+* Queries / Aggregations / Updates containing `SpEL` cannot be generated.
+* Limited `Collation` detection.
+
+**Excluded methods**
+
+* `CrudRepository` and other base interface methods
+* Querydsl and Query by Example methods
+* Methods whose implementation would be overly complex
+* Query Methods obtaining MQL from a file
+** Geospatial Queries
diff --git a/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc b/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc
index d76266c36a..3b5b4e49fe 100644
--- a/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc
+++ b/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc
@@ -465,7 +465,7 @@ This can be a single value (the _id_ by default), or a `Document` provided via a
 * `@Transient`: By default, all fields are mapped to the document.
 This annotation excludes the field where it is applied from being stored in the database.
 Transient properties cannot be used within a persistence constructor as the converter cannot materialize a value for the constructor argument.
-* `@PersistenceConstructor`: Marks a given constructor - even a package protected one - to use when instantiating the object from the database.
+* `@PersistenceCreator`: Marks a given constructor or a `static` factory method - even a package protected one - to use when instantiating the object from the database.
 Constructor arguments are mapped by name to the key values in the retrieved Document.
 * `@Value`: This annotation is part of the Spring Framework . Within the mapping framework it can be applied to constructor arguments.
 This lets you use a Spring Expression Language statement to transform a key's value retrieved in the database before it is used to construct a domain object.
@@ -513,7 +513,7 @@ public class Person<T extends Address> {
     this.ssn = ssn;
   }
 
-  @PersistenceConstructor
+  @PersistenceCreator
   public Person(Integer ssn, String firstName, String lastName, Integer age, T address) {
     this.ssn = ssn;
     this.firstName = firstName;
@@ -673,7 +673,7 @@ Increased levels of nesting increase the complexity of the aggregation expressio
 [[mapping-custom-object-construction]]
 === Customized Object Construction
 
-The mapping subsystem allows the customization of the object construction by annotating a constructor with the `@PersistenceConstructor` annotation.
+The mapping subsystem allows the customization of the object construction by annotating a constructor with the `@PersistenceCreator` annotation.
 The values to be used for the constructor parameters are resolved in the following way:
 
 * If a parameter is annotated with the `@Value` annotation, the given expression is evaluated and the result is used as the parameter value.
@@ -706,7 +706,7 @@ OrderItem item = converter.read(OrderItem.class, input);
 
 NOTE: The SpEL expression in the `@Value` annotation of the `quantity` parameter falls back to the value `0` if the given property path cannot be resolved.
 
-Additional examples for using the `@PersistenceConstructor` annotation can be found in the https://github.com/spring-projects/spring-data-mongodb/blob/master/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java[MappingMongoConverterUnitTests] test suite.
+Additional examples for using the `@PersistenceCreator` annotation can be found in the https://github.com/spring-projects/spring-data-mongodb/blob/master/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java[MappingMongoConverterUnitTests] test suite.
 
 [[mapping-usage-events]]
 === Mapping Framework Events
diff --git a/src/main/antora/modules/ROOT/pages/mongodb/mongo-encryption.adoc b/src/main/antora/modules/ROOT/pages/mongodb/mongo-encryption.adoc
index 98a6d2478a..14e866cf14 100644
--- a/src/main/antora/modules/ROOT/pages/mongodb/mongo-encryption.adoc
+++ b/src/main/antora/modules/ROOT/pages/mongodb/mongo-encryption.adoc
@@ -1,8 +1,8 @@
 [[mongo.encryption]]
-= Encryption (CSFLE)
+= Encryption
 
 Client Side Encryption is a feature that encrypts data in your application before it is sent to MongoDB.
-We recommend you get familiar with the concepts, ideally from the https://www.mongodb.com/docs/manual/core/csfle/[MongoDB Documentation] to learn more about its capabilities and restrictions before you continue applying Encryption through Spring Data.
+We recommend you get familiar with the concepts, ideally from the https://www.mongodb.com/docs/manual/core/security-in-use-encryption/[MongoDB Documentation] to learn more about its capabilities and restrictions before you continue applying Encryption through Spring Data.
 
 [NOTE]
 ====
@@ -11,8 +11,13 @@ MongoDB does not support encryption for all field types.
 Specific data types require deterministic encryption to preserve equality comparison functionality.
 ====
 
+== Client Side Field Level Encryption (CSFLE)
+
+Choosing CSFLE gives you full flexibility and allows you to use different keys for a single field, eg. in a one key per tenant scenario. +
+Please make sure to consult the https://www.mongodb.com/docs/manual/core/csfle/[MongoDB CSFLE Documentation] before you continue reading.
+
 [[mongo.encryption.automatic]]
-== Automatic Encryption
+=== Automatic Encryption (CSFLE)
 
 MongoDB supports https://www.mongodb.com/docs/manual/core/csfle/[Client-Side Field Level Encryption] out of the box using the MongoDB driver with its Automatic Encryption feature.
 Automatic Encryption requires a xref:mongodb/mapping/mapping-schema.adoc[JSON Schema] that allows to perform encrypted read and write operations without the need to provide an explicit en-/decryption step.
@@ -47,7 +52,7 @@ MongoClientSettingsBuilderCustomizer customizer(MappingContext mappingContext) {
 ----
 
 [[mongo.encryption.explicit]]
-== Explicit Encryption
+=== Explicit Encryption (CSFLE)
 
 Explicit encryption uses the MongoDB driver's encryption library (`org.mongodb:mongodb-crypt`) to perform encryption and decryption tasks.
 The `@ExplicitEncrypted` annotation is a combination of the `@Encrypted` annotation used for xref:mongodb/mapping/mapping-schema.adoc#mongo.jsonSchema.encrypted-fields[JSON Schema creation] and a xref:mongodb/mapping/property-converters.adoc[Property Converter].
@@ -114,8 +119,147 @@ By default, the `@ExplicitEncrypted(value=…)` attribute references a `MongoEnc
 It is possible to change the default implementation and exchange it with any `PropertyValueConverter` implementation by providing the according type reference.
 To learn more about custom `PropertyValueConverters` and the required configuration, please refer to the xref:mongodb/mapping/property-converters.adoc[Property Converters - Mapping specific fields] section.
 
+[[mongo.encryption.queryable]]
+== Queryable Encryption (QE)
+
+Choosing QE enables you to run different types of queries, like _range_ or _equality_, against encrypted fields. +
+Please make sure to consult the https://www.mongodb.com/docs/manual/core/queryable-encryption/[MongoDB QE Documentation] before you continue reading to learn more about QE features and limitations.
+
+=== Collection Setup
+
+Queryable Encryption requires upfront declaration of certain aspects allowed within an actual query against an encrypted field.
+The information covers the algorithm in use as well as allowed query types along with their attributes and must be provided when creating the collection.
+
+`MongoOperations#createCollection(...)` can be used to do the initial setup for collections utilizing QE.
+The configuration for QE via Spring Data uses the same building blocks (a xref:mongodb/mapping/mapping-schema.adoc#mongo.jsonSchema.encrypted-fields[JSON Schema creation]) as CSFLE, converting the schema/properties into the configuration format required by MongoDB.
+
+[tabs]
+======
+Manual Collection Setup::
++
+====
+[source,java,indent=0,subs="verbatim,quotes",role="primary"]
+----
+CollectionOptions collectionOptions = CollectionOptions.encryptedCollection(options -> options
+	.queryable(encrypted(string("ssn")).algorithm("Indexed"), equality().contention(0))
+	.queryable(encrypted(int32("age")).algorithm("Range"), range().contention(8).min(0).max(150))
+	.queryable(encrypted(int64("address.sign")).algorithm("Range"), range().contention(2).min(-10L).max(10L))
+);
+
+mongoTemplate.createCollection(Patient.class, collectionOptions); <1>
+----
+<1> Using the template to create the collection may prevent capturing generated keyIds. In this case render the `Document` from the options and use the `createEncryptedCollection(...)` method via the encryption library.
+====
+
+Derived Collection Setup::
++
+====
+[source,java,indent=0,subs="verbatim,quotes",role="secondary"]
+----
+class Patient {
+
+    @Id String id;
+
+    @Encrypted(algorithm = "Indexed") //
+    @Queryable(queryType = "equality", contentionFactor = 0)
+    String ssn;
+
+    @RangeEncrypted(contentionFactor = 8, rangeOptions = "{ 'min' : 0, 'max' : 150 }")
+    Integer age;
+
+    Address address;
+}
+
+MongoJsonSchema patientSchema = MongoJsonSchemaCreator.create(mappingContext)
+    .filter(MongoJsonSchemaCreator.encryptedOnly())
+    .createSchemaFor(Patient.class);
+
+CollectionOptions collectionOptions = CollectionOptions.encryptedCollection(patientSchema);
+
+mongoTemplate.createCollection(Patient.class, collectionOptions); <1>
+----
+<1> Using the template to create the collection may prevent capturing generated keyIds. In this case render the `Document` from the options and use the `createEncryptedCollection(...)` method via the encryption library.
+
+The `Queryable` annotation allows to define allowed query types for encrypted fields.
+`@RangeEncrypted` is a combination of `@Encrypted` and `@Queryable` for fields allowing `range` queries.
+It is possible to create custom annotations out of the provided ones.
+====
+
+MongoDB Collection Info::
++
+====
+[source,java,indent=0,subs="verbatim,quotes",role="thrid"]
+----
+{
+    name: 'patient',
+    type: 'collection',
+    options: {
+      encryptedFields: {
+        escCollection: 'enxcol_.test.esc',
+        ecocCollection: 'enxcol_.test.ecoc',
+        fields: [
+          {
+            keyId: ...,
+            path: 'ssn',
+            bsonType: 'string',
+            queries: [ { queryType: 'equality', contention: Long('0') } ]
+          },
+          {
+            keyId: ...,
+            path: 'age',
+            bsonType: 'int',
+            queries: [ { queryType: 'range', contention: Long('8'), min: 0, max: 150 } ]
+          },
+          {
+            keyId: ...,
+            path: 'address.sign',
+            bsonType: 'long',
+            queries: [ { queryType: 'range', contention: Long('2'), min: Long('-10'), max: Long('10') } ]
+          }
+        ]
+      }
+    }
+}
+----
+====
+======
+
+[NOTE]
+====
+- It is not possible to use both QE and CSFLE within the same collection.
+- It is not possible to query a `range` indexed field with an `equality` operator.
+- It is not possible to query an `equality` indexed field with a `range` operator.
+- It is not possible to set `bypassAutoEncrytion(true)`.
+- It is not possible to use self maintained encryption keys via `@Encrypted` in combination with Queryable Encryption.
+- Contention is only optional on the server side, the clients requires you to set the value (Default us `8`).
+- Additional options for eg. `min` and `max` need to match the actual field type. Make sure to use `$numberLong` etc. to ensure target types when parsing bson String.
+- Queryable Encryption will an extra field `__safeContent__` to each of your documents.
+Unless explicitly excluded the field will be loaded into memory when retrieving results.
+====
+
+[[mongo.encryption.queryable.automatic]]
+=== Automatic Encryption (QE)
+
+MongoDB supports Queryable Encryption out of the box using the MongoDB driver with its Automatic Encryption feature.
+Automatic Encryption requires a xref:mongodb/mapping/mapping-schema.adoc[JSON Schema] that allows to perform encrypted read and write operations without the need to provide an explicit en-/decryption step.
+
+All you need to do is create the collection according to the MongoDB documentation.
+You may utilize techniques to create the required configuration outlined in the section above.
+
+[[mongo.encryption.queryable.manual]]
+=== Explicit Encryption (QE)
+
+Explicit encryption uses the MongoDB driver's encryption library (`org.mongodb:mongodb-crypt`) to perform encryption and decryption tasks based on the meta information provided by annotation within the domain model.
+
+[NOTE]
+====
+There is no official support for using Explicit Queryable Encryption.
+The audacious user may combine `@Encrypted` and `@Queryable` with `@ValueConverter(MongoEncryptionConverter.class)` at their own risk.
+====
+
 [[mongo.encryption.explicit-setup]]
-=== MongoEncryptionConverter Setup
+[[mongo.encryption.converter-setup]]
+== MongoEncryptionConverter Setup
 
 The converter setup for `MongoEncryptionConverter` requires a few steps as several components are involved.
 The bean setup consists of the following:
@@ -124,7 +268,6 @@ The bean setup consists of the following:
 2. A `MongoEncryptionConverter` instance configured with `ClientEncryption` and a `EncryptionKeyResolver`.
 3. A `PropertyValueConverterFactory` that uses the registered `MongoEncryptionConverter` bean.
 
-A side effect of using annotated key resolution is that the `@ExplicitEncrypted` annotation does not need to specify an alt key name.
 The `EncryptionKeyResolver` uses an `EncryptionContext` providing access to the property allowing for dynamic DEK resolution.
 
 .Sample MongoEncryptionConverter Configuration
diff --git a/src/main/antora/modules/ROOT/pages/mongodb/mongo-search-indexes.adoc b/src/main/antora/modules/ROOT/pages/mongodb/mongo-search-indexes.adoc
index 9b6bfcf095..7fc51de007 100644
--- a/src/main/antora/modules/ROOT/pages/mongodb/mongo-search-indexes.adoc
+++ b/src/main/antora/modules/ROOT/pages/mongodb/mongo-search-indexes.adoc
@@ -25,7 +25,7 @@ Java::
 [source,java,indent=0,subs="verbatim,quotes",role="primary"]
 ----
 VectorIndex index = new VectorIndex("vector_index")
-  .addVector("plotEmbedding"), vector -> vector.dimensions(1536).similarity(COSINE)) <1>
+  .addVector("plotEmbedding", vector -> vector.dimensions(1536).similarity(COSINE)) <1>
   .addFilter("year"); <2>
 
 mongoTemplate.searchIndexOps(Movie.class) <3>
@@ -84,7 +84,6 @@ VectorSearchOperation search = VectorSearchOperation.search("vector_index") <1>
   .vector( ... )
   .numCandidates(150)
   .limit(10)
-  .quantization(SCALAR)
   .withSearchScore("score"); <3>
 
 AggregationResults<MovieWithSearchScore> results = mongoTemplate
@@ -107,8 +106,7 @@ db.embedded_movies.aggregate([
       "path": "plot_embedding", <1>
       "queryVector": [ ... ],
       "numCandidates": 150,
-      "limit": 10,
-      "quantization": "scalar"
+      "limit": 10
     }
   },
   {
diff --git a/src/main/antora/modules/ROOT/pages/mongodb/repositories/vector-search.adoc b/src/main/antora/modules/ROOT/pages/mongodb/repositories/vector-search.adoc
new file mode 100644
index 0000000000..9129c80a21
--- /dev/null
+++ b/src/main/antora/modules/ROOT/pages/mongodb/repositories/vector-search.adoc
@@ -0,0 +1,8 @@
+:vector-search-intro-include: partial$vector-search-intro-include.adoc
+:vector-search-model-include: partial$vector-search-model-include.adoc
+:vector-search-repository-include: partial$vector-search-repository-include.adoc
+:vector-search-scoring-include: partial$vector-search-scoring-include.adoc
+:vector-search-method-derived-include: partial$vector-search-method-derived-include.adoc
+:vector-search-method-annotated-include: partial$vector-search-method-annotated-include.adoc
+
+include::partial$/vector-search.adoc[]
diff --git a/src/main/antora/modules/ROOT/pages/mongodb/template-api.adoc b/src/main/antora/modules/ROOT/pages/mongodb/template-api.adoc
index f2a7a19bd6..ece61559ec 100644
--- a/src/main/antora/modules/ROOT/pages/mongodb/template-api.adoc
+++ b/src/main/antora/modules/ROOT/pages/mongodb/template-api.adoc
@@ -116,6 +116,19 @@ WARNING: Projections must not be applied to xref:mongodb/mapping/document-refere
 
 You can switch between retrieving a single entity and retrieving multiple entities as a `List` or a `Stream` through the terminating methods: `first()`, `one()`, `all()`, or `stream()`.
 
+Results can be contextually post-processed by using a `QueryResultConverter` that has access to both the raw result `Document` and the already mapped object by calling `map(...)` as outlined below.
+
+[source,java]
+====
+----
+List<Optional<Jedi>> result = template.query(Person.class)
+    .as(Jedi.class)
+    .matching(query(where("firstname").is("luke")))
+    .map((document, reader) -> Optional.of(reader.get()))
+    .all();
+----
+====
+
 When writing a geo-spatial query with `near(NearQuery)`, the number of terminating methods is altered to include only the methods that are valid for running a `geoNear` command in MongoDB (fetching entities as a `GeoResult` within `GeoResults`), as the following example shows:
 
 [tabs]
diff --git a/src/main/antora/modules/ROOT/pages/mongodb/template-query-operations.adoc b/src/main/antora/modules/ROOT/pages/mongodb/template-query-operations.adoc
index a424748205..697af23a9e 100644
--- a/src/main/antora/modules/ROOT/pages/mongodb/template-query-operations.adoc
+++ b/src/main/antora/modules/ROOT/pages/mongodb/template-query-operations.adoc
@@ -342,7 +342,7 @@ public class Venue {
   private String name;
   private double[] location;
 
-  @PersistenceConstructor
+  @PersistenceCreator
   Venue(String name, double[] location) {
     super();
     this.name = name;
diff --git a/src/main/antora/modules/ROOT/pages/repositories/core-concepts.adoc b/src/main/antora/modules/ROOT/pages/repositories/core-concepts.adoc
index 1a4af7a60b..7d31acb2d4 100644
--- a/src/main/antora/modules/ROOT/pages/repositories/core-concepts.adoc
+++ b/src/main/antora/modules/ROOT/pages/repositories/core-concepts.adoc
@@ -2,11 +2,3 @@ include::{commons}@data-commons::page$repositories/core-concepts.adoc[]
 
 [[mongodb.entity-persistence.state-detection-strategies]]
 include::{commons}@data-commons::page$is-new-state-detection.adoc[leveloffset=+1]
-
-[NOTE]
-====
-Cassandra provides no means to generate identifiers upon inserting data.
-As consequence, entities must be associated with identifier values.
-Spring Data defaults to identifier inspection to determine whether an entity is new.
-If you want to use xref:mongodb/auditing.adoc[auditing] make sure to either use xref:mongodb/template-crud-operations.adoc#mongo-template.optimistic-locking[Optimistic Locking] or implement `Persistable` for proper entity state detection.
-====
diff --git a/src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc b/src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc
index 3bc7648154..75dcea1e4f 100644
--- a/src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc
+++ b/src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc
@@ -169,7 +169,6 @@ Maven::
         <groupId>com.querydsl</groupId>
         <artifactId>querydsl-mongodb</artifactId>
         <version>${querydslVersion}</version>
-        <classifier>jakarta</classifier>
 
         <!-- Recommended: Exclude the mongo-java-driver to avoid version conflicts -->
         <exclusions>
@@ -216,7 +215,7 @@ Gradle::
 [source,groovy,indent=0,subs="verbatim,quotes",role="secondary"]
 ----
 dependencies {
-    implementation 'com.querydsl:querydsl-mongodb:${querydslVersion}:jakarta'
+    implementation 'com.querydsl:querydsl-mongodb:${querydslVersion}'
 
     annotationProcessor 'com.querydsl:querydsl-apt:${querydslVersion}:jakarta'
     annotationProcessor 'org.springframework.data:spring-data-mongodb'
@@ -235,6 +234,8 @@ tasks.withType(JavaCompile).configureEach {
 ======
 
 Note that the setup above shows the simplest usage omitting any other options or dependencies that your project might require.
+This way of configuring annotation processing disables Java's annotation processor scanning because MongoDB requires specifying `-processor` by class name.
+If you're using other annotation processors, you need to add them to the list of `-processor`/`annotationProcessors` as well.
 
 include::{commons}@data-commons::page$repositories/core-extensions-web.adoc[leveloffset=1]
 
diff --git a/src/main/antora/modules/ROOT/pages/repositories/query-methods-details.adoc b/src/main/antora/modules/ROOT/pages/repositories/query-methods-details.adoc
index dfe4814955..614da0b059 100644
--- a/src/main/antora/modules/ROOT/pages/repositories/query-methods-details.adoc
+++ b/src/main/antora/modules/ROOT/pages/repositories/query-methods-details.adoc
@@ -1 +1,2 @@
+:feature-scroll:
 include::{commons}@data-commons::page$repositories/query-methods-details.adoc[]
diff --git a/src/main/antora/modules/ROOT/partials/vector-search-intro-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-intro-include.adoc
new file mode 100644
index 0000000000..355bccf4e3
--- /dev/null
+++ b/src/main/antora/modules/ROOT/partials/vector-search-intro-include.adoc
@@ -0,0 +1 @@
+To use Vector Search with MongoDB, you need a MongoDB Atlas instance that is either running in the cloud or by using https://www.mongodb.com/docs/atlas/cli/current/atlas-cli-deploy-docker/[Docker].
diff --git a/src/main/antora/modules/ROOT/partials/vector-search-method-annotated-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-method-annotated-include.adoc
new file mode 100644
index 0000000000..252437f0b7
--- /dev/null
+++ b/src/main/antora/modules/ROOT/partials/vector-search-method-annotated-include.adoc
@@ -0,0 +1,23 @@
+Annotated search methods use the `@VectorSearch` annotation to define parameters for the https://www.mongodb.com/docs/upcoming/reference/operator/aggregation/vectorSearch/[`$vectorSearch`] aggregation stage.
+
+.Using `@VectorSearch` Search Methods
+====
+[source,java]
+----
+interface CommentRepository extends Repository<Comment, String> {
+
+  @VectorSearch(indexName = "cos-index", filter = "{country: ?0}", limit="100", numCandidates="2000")
+  SearchResults<Comment> searchAnnotatedByCountryAndEmbeddingWithin(String country, Vector embedding,
+      Score distance);
+
+  @VectorSearch(indexName = "my-index", filter = "{country: ?0}", limit="?3", numCandidates = "#{#limit * 20}",
+				searchType = VectorSearchOperation.SearchType.ANN)
+  List<Comment> findAnnotatedByCountryAndEmbeddingWithin(String country, Vector embedding, Score distance, int limit);
+}
+----
+====
+
+Annotated Search Methods can define `filter` for pre-filter usage.
+
+`filter`, `limit`, and `numCandidates` support xref:page$mongodb/value-expressions.adoc[Value Expressions] allowing references to search method arguments.
+
diff --git a/src/main/antora/modules/ROOT/partials/vector-search-method-derived-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-method-derived-include.adoc
new file mode 100644
index 0000000000..f2b006b8e4
--- /dev/null
+++ b/src/main/antora/modules/ROOT/partials/vector-search-method-derived-include.adoc
@@ -0,0 +1,21 @@
+MongoDB Search methods must use the `@VectorSearch` annotation to define the index name for the https://www.mongodb.com/docs/upcoming/reference/operator/aggregation/vectorSearch/[`$vectorSearch`] aggregation stage.
+
+.Using `Near` and `Within` Keywords in Repository Search Methods
+====
+[source,java]
+----
+interface CommentRepository extends Repository<Comment, String> {
+
+  @VectorSearch(indexName = "my-index", numCandidates="200")
+  SearchResults<Comment> searchTop10ByEmbeddingNear(Vector vector, Score score);
+
+  @VectorSearch(indexName = "my-index", numCandidates="200")
+  SearchResults<Comment> searchTop10ByEmbeddingWithin(Vector vector, Range<Similarity> range);
+
+  @VectorSearch(indexName = "my-index", numCandidates="200")
+  SearchResults<Comment> searchTop10ByCountryAndEmbeddingWithin(String country, Vector vector, Range<Similarity> range);
+}
+----
+====
+
+Derived Search Methods can define domain model attributes to create the pre-filter for indexed fields.
diff --git a/src/main/antora/modules/ROOT/partials/vector-search-model-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-model-include.adoc
new file mode 100644
index 0000000000..e657f3aa63
--- /dev/null
+++ b/src/main/antora/modules/ROOT/partials/vector-search-model-include.adoc
@@ -0,0 +1,15 @@
+====
+[source,java]
+----
+class Comment {
+
+  @Id String id;
+  String country;
+  String comment;
+
+  Vector embedding;
+
+  // getters, setters, …
+}
+----
+====
diff --git a/src/main/antora/modules/ROOT/partials/vector-search-repository-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-repository-include.adoc
new file mode 100644
index 0000000000..0e987fc1c5
--- /dev/null
+++ b/src/main/antora/modules/ROOT/partials/vector-search-repository-include.adoc
@@ -0,0 +1,25 @@
+.Using `SearchResult<T>` in a Repository Search Method
+====
+[source,java]
+----
+interface CommentRepository extends Repository<Comment, String> {
+
+  @VectorSearch(indexName = "my-index", numCandidates="#{#limit.max() * 20}")
+  SearchResults<Comment> searchByCountryAndEmbeddingNear(String country, Vector vector, Score score,
+    Limit limit);
+
+  @VectorSearch(indexName = "my-index", limit="10", numCandidates="200")
+  SearchResults<Comment> searchByCountryAndEmbeddingWithin(String country, Vector embedding,
+      Score score);
+
+}
+
+SearchResults<Comment> results = repository.searchByCountryAndEmbeddingNear("en", Vector.of(…), Score.of(0.9), Limit.of(10));
+----
+====
+
+[TIP]
+====
+The MongoDB https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/[vector search aggregation] stage defines a set of required arguments and restrictions.
+Please make sure to follow the guidelines and make sure to provide required arguments like `limit`.
+====
diff --git a/src/main/antora/modules/ROOT/partials/vector-search-scoring-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-scoring-include.adoc
new file mode 100644
index 0000000000..313d8bf394
--- /dev/null
+++ b/src/main/antora/modules/ROOT/partials/vector-search-scoring-include.adoc
@@ -0,0 +1,32 @@
+MongoDB reports the score directly as similarity value.
+The scoring function must be specified in the index and therefore, Vector search methods do not consider the `Score.scoringFunction`.
+The scoring function defaults to `ScoringFunction.unspecified()` as there is no information inside of search results how the score has been computed.
+
+.Using `Score` and `Similarity` in a Repository Search Methods
+====
+[source,java]
+----
+interface CommentRepository extends Repository<Comment, String> {
+
+  @VectorSearch(…)
+  SearchResults<Comment> searchTop10ByEmbeddingNear(Vector vector, Score similarity);
+
+  @VectorSearch(…)
+  SearchResults<Comment> searchTop10ByEmbeddingNear(Vector vector, Similarity similarity);
+
+  @VectorSearch(…)
+  SearchResults<Comment> searchTop10ByEmbeddingNear(Vector vector, Range<Similarity> range);
+}
+
+repository.searchByEmbeddingNear(Vector.of(…), Score.of(0.9));                <1>
+
+repository.searchByEmbeddingNear(Vector.of(…), Similarity.of(0.9));           <2>
+
+repository.searchByEmbeddingNear(Vector.of(…), Similarity.between(0.5, 1));   <3>
+----
+
+<1> Run a search and return results with a similarity of `0.9` or greater.
+<2> Return results with a similarity of `0.9` or greater.
+<3> Return results with a similarity of between `0.5` and `1.0`  or greater.
+====
+
diff --git a/src/main/antora/modules/ROOT/partials/vector-search.adoc b/src/main/antora/modules/ROOT/partials/vector-search.adoc
new file mode 100644
index 0000000000..15e32dccee
--- /dev/null
+++ b/src/main/antora/modules/ROOT/partials/vector-search.adoc
@@ -0,0 +1,167 @@
+[[vector-search]]
+= Vector Search
+
+With the rise of Generative AI, Vector databases have gained strong traction in the world of databases.
+These databases enable efficient storage and querying of high-dimensional vectors, making them well-suited for tasks such as semantic search, recommendation systems, and natural language understanding.
+
+Vector search is a technique that retrieves semantically similar data by comparing vector representations (also known as embeddings) rather than relying on traditional exact-match queries.
+This approach enables intelligent, context-aware applications that go beyond keyword-based retrieval.
+
+In the context of Spring Data, vector search opens new possibilities for building intelligent, context-aware applications, particularly in domains like natural language processing, recommendation systems, and generative AI.
+By modelling vector-based querying using familiar repository abstractions, Spring Data allows developers to seamlessly integrate similarity-based vector-capable databases with the simplicity and consistency of the Spring Data programming model.
+
+ifdef::vector-search-intro-include[]
+include::{vector-search-intro-include}[]
+endif::[]
+
+[[vector-search.model]]
+== Vector Model
+
+To support vector search in a type-safe and idiomatic way, Spring Data introduces the following core abstractions:
+
+* <<vector-search.model.vector,`Vector`>>
+* <<vector-search.model.search-result,`SearchResults<T>` and `SearchResult<T>`>>
+* <<vector-search.model.scoring,`Score`, `Similarity` and Scoring Functions>>
+
+[[vector-search.model.vector]]
+=== `Vector`
+
+The `Vector` type represents an n-dimensional numerical embedding, typically produced by embedding models.
+In Spring Data, it is defined as a lightweight wrapper around an array of floating-point numbers, ensuring immutability and consistency.
+This type can be used as an input for search queries or as a property on a domain entity to store the associated vector representation.
+
+====
+[source,java]
+----
+Vector vector = Vector.of(0.23f, 0.11f, 0.77f);
+----
+====
+
+Using `Vector` in your domain model removes the need to work with raw arrays or lists of numbers, providing a more type-safe and expressive way to handle vector data.
+This abstraction also allows for easy integration with various vector databases and libraries.
+It also allows for implementing vendor-specific optimizations such as binary or quantized vectors that do not map to a standard floating point (`float` and `double` as of https://en.wikipedia.org/wiki/IEEE_754[IEEE 754]) representation.
+A domain object can have a vector property, which can be used for similarity searches.
+Consider the following example:
+
+ifdef::vector-search-model-include[]
+include::{vector-search-model-include}[]
+endif::[]
+
+NOTE: Associating a vector with a domain object results in the vector being loaded and stored as part of the entity lifecycle, which may introduce additional overhead on retrieval and persistence operations.
+
+[[vector-search.model.search-result]]
+=== Search Results
+
+The `SearchResult<T>` type encapsulates the results of a vector similarity query.
+It includes both the matched domain object and a relevance score that indicates how closely it matches the query vector.
+This abstraction provides a structured way to handle result ranking and enables developers to easily work with both the data and its contextual relevance.
+
+ifdef::vector-search-repository-include[]
+include::{vector-search-repository-include}[]
+endif::[]
+
+In this example, the `searchByCountryAndEmbeddingNear` method returns a `SearchResults<Comment>` object, which contains a list of `SearchResult<Comment>` instances.
+Each result includes the matched `Comment` entity and its relevance score.
+
+Relevance score is a numerical value that indicates how closely the matched vector aligns with the query vector.
+Depending on whether a score represents distance or similarity a higher score can mean a closer match or a more distant one.
+
+The scoring function used to calculate this score can vary based on the underlying database, index or input parameters.
+
+[[vector-search.model.scoring]]
+=== Score, Similarity, and Scoring Functions
+
+The `Score` type holds a numerical value indicating the relevance of a search result.
+It can be used to rank results based on their similarity to the query vector.
+The `Score` type is typically a floating-point number, and its interpretation (higher is better or lower is better) depends on the specific similarity function used.
+Scores are a by-product of vector search and are not required for a successful search operation.
+Score values are not part of a domain model and therefore represented best as out-of-band data.
+
+Generally, a Score is computed by a `ScoringFunction`.
+The actual scoring function used to calculate this score can depends on the underlying database and can be obtained from a search index or input parameters.
+
+Spring Data support declares constants for commonly used functions such as:
+
+Euclidean Distance:: Calculates the straight-line distance in n-dimensional space involving the square root of the sum of squared differences.
+Cosine Similarity:: Measures the angle between two vectors by calculating the Dot product first and then normalizing its result by dividing by the product of their lengths.
+Dot Product:: Computes the sum of element-wise multiplications.
+
+The choice of similarity function can impact both the performance and semantics of the search and is often determined by the underlying database or index being used.
+Spring Data adopts to the database's native scoring function capabilities and whether the score can be used to limit results.
+
+ifdef::vector-search-scoring-include[]
+include::{vector-search-scoring-include}[]
+endif::[]
+
+[[vector-search.methods]]
+== Vector Search Methods
+
+Vector search methods are defined in repositories using the same conventions as standard Spring Data query methods.
+These methods return `SearchResults<T>` and require a `Vector` parameter to define the query vector.
+The actual implementation depends on the actual internals of the underlying data store and its capabilities around vector search.
+
+NOTE: If you are new to Spring Data repositories, make sure to familiarize yourself with the xref:repositories/core-concepts.adoc[basics of repository definitions and query methods].
+
+Generally, you have the choice of declaring a search method using two approaches:
+
+* Query Derivation
+* Declaring a String-based Query
+
+Vector Search methods must declare a `Vector` parameter to define the query vector.
+
+[[vector-search.method.derivation]]
+=== Derived Search Methods
+
+A derived search method uses the name of the method to derive the query.
+Vector Search supports the following keywords to run a Vector search when declaring a search method:
+
+.Query predicate keywords
+[options="header",cols="1,3"]
+|===============
+|Logical keyword|Keyword expressions
+|`NEAR`|`Near`, `IsNear`
+|`WITHIN`|`Within`, `IsWithin`
+|===============
+
+ifdef::vector-search-method-derived-include[]
+include::{vector-search-method-derived-include}[]
+endif::[]
+
+Derived search methods are typically easier to read and maintain, as they rely on the method name to express the query intent.
+However, a derived search method requires either to declare a `Score`, `Range<Score>` or `ScoreFunction` as second argument to the `Near`/`Within` keyword to limit search results by their score.
+
+[[vector-search.method.string]]
+=== Annotated Search Methods
+
+Annotated methods provide full control over the query semantics and parameters.
+Unlike derived methods, they do not rely on method name conventions.
+
+ifdef::vector-search-method-annotated-include[]
+include::{vector-search-method-annotated-include}[]
+endif::[]
+
+With more control over the actual query, Spring Data can make fewer assumptions about the query and its parameters.
+For example, `Similarity` normalization uses the native score function within the query to normalize the given similarity into a score predicate value and vice versa.
+If an annotated query does not define e.g. the score, then the score value in the returned `SearchResult<T>` will be zero.
+
+[[vector-search.method.sorting]]
+=== Sorting
+
+By default, search results are ordered according to their score.
+You can override sorting by using the `Sort` parameter:
+
+.Using `Sort` in Repository Search Methods
+====
+[source,java]
+----
+interface CommentRepository extends Repository<Comment, String> {
+
+  SearchResults<Comment> searchByEmbeddingNearOrderByCountry(Vector vector, Score score);
+
+  SearchResults<Comment> searchByEmbeddingWithin(Vector vector, Score score, Sort sort);
+}
+----
+====
+
+Please note that custom sorting does not allow expressing the score as a sorting criteria.
+You can only refer to domain properties.
diff --git a/src/main/resources/notice.txt b/src/main/resources/notice.txt
index 61b472b23b..6deef0df9c 100644
--- a/src/main/resources/notice.txt
+++ b/src/main/resources/notice.txt
@@ -1,4 +1,4 @@
-Spring Data MongoDB 4.5 M2 (2025.0.0)
+Spring Data MongoDB 5.0 M2 (2025.1.0)
 Copyright (c) [2010-2019] Pivotal Software, Inc.
 
 This product is licensed to you under the Apache License, Version 2.0 (the "License").