This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven.git

commit 3fde7657f5ff99eb6349b495dd4452f4b5368cb1
Author: Guillaume Nodet <gno...@gmail.com>
AuthorDate: Thu Jan 30 13:23:52 2025 +0100

    [MNG-8540] Add a real caching API and add missing infos to 
ArtifactResolverResult
---
 ...esolverResult.java => WorkspaceRepository.java} |  31 ++-
 .../maven/api/cache/BatchRequestException.java     |  65 ++++++
 .../CacheMetadata.java}                            |  24 +--
 .../org/apache/maven/api/cache/CacheRetention.java |  66 ++++++
 .../MavenExecutionException.java}                  |  22 +-
 .../org/apache/maven/api/cache/RequestCache.java   |  80 +++++++
 .../RequestCacheFactory.java}                      |  25 ++-
 .../org/apache/maven/api/cache/RequestResult.java  |  63 ++++++
 .../org/apache/maven/api/cache/package-info.java   |  55 +++++
 .../api/services/ArtifactResolverException.java    |  10 +-
 .../api/services/ArtifactResolverRequest.java      |   3 +-
 .../maven/api/services/ArtifactResolverResult.java |  98 ++++++++-
 .../maven/api/services/LocalRepositoryManager.java |  18 ++
 .../org/apache/maven/api/services/Sources.java     |  19 +-
 .../repository/LegacyRepositorySystemTest.java     |   7 +-
 .../extensions/BootstrapCoreExtensionManager.java  |  13 +-
 .../impl/ConsumerPomBuilderTest.java               |   2 +
 .../org/apache/maven/model/ModelBuilderTest.java   |   7 +-
 .../org/apache/maven/impl/AbstractSession.java     | 129 ++++++-----
 .../apache/maven/impl/DefaultArtifactResolver.java | 210 ++++++++++++++----
 .../maven/impl/DefaultDependencyResolver.java      |  10 +-
 .../maven/impl/DefaultLocalRepositoryManager.java  |  19 +-
 .../org/apache/maven/impl/InternalSession.java     |  11 +
 .../apache/maven/impl/cache/CachingSupplier.java   |  79 +++++++
 .../maven/impl/cache/DefaultRequestCache.java      | 167 ++++++++++++++
 .../impl/cache/DefaultRequestCacheFactory.java     |  30 +--
 .../apache/maven/impl/cache/WeakIdentityMap.java   | 239 +++++++++++++++++++++
 .../maven/impl/cache/WeakIdentityMapTest.java      | 196 +++++++++++++++++
 28 files changed, 1496 insertions(+), 202 deletions(-)

diff --git 
a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverResult.java
 
b/api/maven-api-core/src/main/java/org/apache/maven/api/WorkspaceRepository.java
similarity index 57%
copy from 
api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverResult.java
copy to 
api/maven-api-core/src/main/java/org/apache/maven/api/WorkspaceRepository.java
index f00f7b2337..f8cd2bac6d 100644
--- 
a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverResult.java
+++ 
b/api/maven-api-core/src/main/java/org/apache/maven/api/WorkspaceRepository.java
@@ -16,30 +16,25 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.maven.api.services;
+package org.apache.maven.api;
 
-import java.nio.file.Path;
-import java.util.Collection;
-
-import org.apache.maven.api.Artifact;
-import org.apache.maven.api.DownloadedArtifact;
-import org.apache.maven.api.annotations.Experimental;
 import org.apache.maven.api.annotations.Nonnull;
-import org.apache.maven.api.annotations.Nullable;
 
 /**
- * The Artifact Result
- *
- * @since 4.0.0
+ * Represents a repository backed by an IDE workspace, the output of a build 
session,
+ * or similar ad-hoc collections of artifacts. This repository is considered 
read-only
+ * within the context of a session, meaning it can only be used for artifact 
resolution,
+ * not for installation or deployment. This interface does not provide direct 
access
+ * to artifacts; that functionality is handled by a {@code WorkspaceReader}.
  */
-@Experimental
-public interface ArtifactResolverResult extends 
Result<ArtifactResolverRequest> {
+public interface WorkspaceRepository extends Repository {
+
     /**
-     * @return {@link Artifact}
+     * {@return the type of the repository, i.e. "workspace"}
      */
     @Nonnull
-    Collection<DownloadedArtifact> getArtifacts();
-
-    @Nullable
-    Path getPath(Artifact artifact);
+    @Override
+    default String getType() {
+        return "workspace";
+    }
 }
diff --git 
a/api/maven-api-core/src/main/java/org/apache/maven/api/cache/BatchRequestException.java
 
b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/BatchRequestException.java
new file mode 100644
index 0000000000..4757d60c7a
--- /dev/null
+++ 
b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/BatchRequestException.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *   http://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.apache.maven.api.cache;
+
+import java.util.List;
+
+import org.apache.maven.api.annotations.Experimental;
+import org.apache.maven.api.services.Request;
+import org.apache.maven.api.services.Result;
+
+/**
+ * Exception thrown when a batch request operation fails. This exception 
contains the results
+ * of all requests that were attempted, including both successful and failed 
operations.
+ * <p>
+ * The exception provides access to detailed results through {@link 
#getResults()}, allowing
+ * callers to determine which specific requests failed and why.
+ *
+ * @since 4.0.0
+ */
+@Experimental
+public class BatchRequestException extends RuntimeException {
+
+    private final List<RequestResult<?, ?>> results;
+
+    /**
+     * Constructs a new BatchRequestException with the specified message and 
results.
+     *
+     * @param <REQ> The type of the request
+     * @param <REP> The type of the response
+     * @param message The error message describing the batch operation failure
+     * @param allResults List of results from all attempted requests in the 
batch
+     */
+    public <REQ extends Request<?>, REP extends Result<REQ>> 
BatchRequestException(
+            String message, List<RequestResult<REQ, REP>> allResults) {
+        super(message);
+        this.results = List.copyOf(allResults);
+    }
+
+    /**
+     * Returns the list of results from all requests that were part of the 
batch operation.
+     * Each result contains the original request, the response (if 
successful), and any error
+     * that occurred during processing.
+     *
+     * @return An unmodifiable list of request results
+     */
+    public List<RequestResult<?, ?>> getResults() {
+        return results;
+    }
+}
diff --git 
a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverResult.java
 
b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/CacheMetadata.java
similarity index 66%
copy from 
api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverResult.java
copy to 
api/maven-api-core/src/main/java/org/apache/maven/api/cache/CacheMetadata.java
index f00f7b2337..ecc9920b22 100644
--- 
a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverResult.java
+++ 
b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/CacheMetadata.java
@@ -16,30 +16,26 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.maven.api.services;
+package org.apache.maven.api.cache;
 
-import java.nio.file.Path;
-import java.util.Collection;
-
-import org.apache.maven.api.Artifact;
-import org.apache.maven.api.DownloadedArtifact;
 import org.apache.maven.api.annotations.Experimental;
-import org.apache.maven.api.annotations.Nonnull;
 import org.apache.maven.api.annotations.Nullable;
 
 /**
- * The Artifact Result
+ * Interface defining metadata for cache behavior and lifecycle management.
+ * Implementations can specify how long cached data should be retained.
  *
  * @since 4.0.0
  */
 @Experimental
-public interface ArtifactResolverResult extends 
Result<ArtifactResolverRequest> {
+public interface CacheMetadata {
+
     /**
-     * @return {@link Artifact}
+     * Returns the cache retention that should be applied to the associated 
data.
+     *
+     * @return The CacheRetention indicating how long data should be retained, 
or null if
+     *         no specific cache retention is defined
      */
-    @Nonnull
-    Collection<DownloadedArtifact> getArtifacts();
-
     @Nullable
-    Path getPath(Artifact artifact);
+    CacheRetention getCacheRetention();
 }
diff --git 
a/api/maven-api-core/src/main/java/org/apache/maven/api/cache/CacheRetention.java
 
b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/CacheRetention.java
new file mode 100644
index 0000000000..4ec20db842
--- /dev/null
+++ 
b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/CacheRetention.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *   http://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.apache.maven.api.cache;
+
+import org.apache.maven.api.annotations.Experimental;
+
+/**
+ * Enumeration defining different retention periods for cached data.
+ * Each value represents a specific scope and lifetime for cached items.
+ *
+ * @since 4.0.0
+ */
+@Experimental
+public enum CacheRetention {
+    /**
+     * Data should be persisted across Maven invocations.
+     * Suitable for:
+     * - Dependency resolution results
+     * - Compilation outputs
+     * - Downloaded artifacts
+     */
+    PERSISTENT,
+
+    /**
+     * Data should be retained for the duration of the current Maven session.
+     * Suitable for:
+     * - Build-wide configuration
+     * - Project model caching
+     * - Inter-module metadata
+     */
+    SESSION_SCOPED,
+
+    /**
+     * Data should only be retained for the current build request.
+     * Suitable for:
+     * - Plugin execution results
+     * - Temporary build artifacts
+     * - Phase-specific data
+     */
+    REQUEST_SCOPED,
+
+    /**
+     * Caching should be disabled for this data.
+     * Suitable for:
+     * - Sensitive information
+     * - Non-deterministic operations
+     * - Debug or development data
+     */
+    DISABLED
+}
diff --git 
a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverException.java
 
b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/MavenExecutionException.java
similarity index 64%
copy from 
api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverException.java
copy to 
api/maven-api-core/src/main/java/org/apache/maven/api/cache/MavenExecutionException.java
index 05831ce172..a8862e2d33 100644
--- 
a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverException.java
+++ 
b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/MavenExecutionException.java
@@ -16,28 +16,26 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.maven.api.services;
-
-import java.io.Serial;
+package org.apache.maven.api.cache;
 
 import org.apache.maven.api.annotations.Experimental;
+import org.apache.maven.api.services.MavenException;
 
 /**
- *
+ * Exception thrown when an error occurs during Maven execution.
+ * This exception wraps the original cause of the execution failure.
  *
  * @since 4.0.0
  */
 @Experimental
-public class ArtifactResolverException extends MavenException {
-
-    @Serial
-    private static final long serialVersionUID = 7252294837746943917L;
+public class MavenExecutionException extends MavenException {
 
     /**
-     * @param message the message for the exception
-     * @param e the exception itself
+     * Constructs a new MavenExecutionException with the specified cause.
+     *
+     * @param cause The underlying exception that caused the execution failure
      */
-    public ArtifactResolverException(String message, Exception e) {
-        super(message, e);
+    public MavenExecutionException(Throwable cause) {
+        super(cause);
     }
 }
diff --git 
a/api/maven-api-core/src/main/java/org/apache/maven/api/cache/RequestCache.java 
b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/RequestCache.java
new file mode 100644
index 0000000000..2a4ad69d83
--- /dev/null
+++ 
b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/RequestCache.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *   http://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.apache.maven.api.cache;
+
+import java.util.List;
+import java.util.function.Function;
+
+import org.apache.maven.api.annotations.Experimental;
+import org.apache.maven.api.services.Request;
+import org.apache.maven.api.services.Result;
+
+/**
+ * Interface for caching request results in Maven. This cache implementation 
provides
+ * methods for executing and optionally caching both single requests and 
batches of requests.
+ * <p>
+ * The cache behavior is determined by the cache retention specified in the 
request's metadata.
+ * Results can be cached at different policies (forever, session, request, or 
not at all)
+ * based on the {@link CacheRetention} associated with the request.
+ *
+ * @since 4.0.0
+ * @see CacheMetadata
+ * @see RequestCacheFactory
+ */
+@Experimental
+public interface RequestCache {
+
+    /**
+     * Executes and optionally caches a request using the provided supplier 
function. If caching is enabled
+     * for this session, the result will be cached and subsequent identical 
requests will return the cached
+     * value without re-executing the supplier.
+     * <p>
+     * The caching behavior is determined by the cache retention specified in 
the request's metadata.
+     * If an error occurs during execution, it will be cached and re-thrown 
for subsequent identical requests.
+     *
+     * @param <REQ> The request type
+     * @param <REP> The response type
+     * @param req The request object used as the cache key
+     * @param supplier The function to execute and cache the result
+     * @return The result from the supplier (either fresh or cached)
+     * @throws RuntimeException Any exception thrown by the supplier will be 
cached and re-thrown on subsequent calls
+     */
+    <REQ extends Request<?>, REP extends Result<REQ>> REP request(REQ req, 
Function<REQ, REP> supplier);
+
+    /**
+     * Executes and optionally caches a batch of requests using the provided 
supplier function.
+     * This method allows for efficient batch processing of multiple requests.
+     * <p>
+     * The implementation may optimize the execution by:
+     * <ul>
+     *   <li>Returning cached results for previously executed requests</li>
+     *   <li>Grouping similar requests for batch processing</li>
+     *   <li>Processing requests in parallel where appropriate</li>
+     * </ul>
+     *
+     * @param <REQ> The request type
+     * @param <REP> The response type
+     * @param req List of requests to process
+     * @param supplier Function to execute the batch of requests
+     * @return List of results corresponding to the input requests
+     * @throws BatchRequestException if any request in the batch fails
+     */
+    <REQ extends Request<?>, REP extends Result<REQ>> List<REP> requests(
+            List<REQ> req, Function<List<REQ>, List<REP>> supplier);
+}
diff --git 
a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverException.java
 
b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/RequestCacheFactory.java
similarity index 63%
copy from 
api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverException.java
copy to 
api/maven-api-core/src/main/java/org/apache/maven/api/cache/RequestCacheFactory.java
index 05831ce172..dff86d521f 100644
--- 
a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverException.java
+++ 
b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/RequestCacheFactory.java
@@ -16,28 +16,27 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.maven.api.services;
-
-import java.io.Serial;
+package org.apache.maven.api.cache;
 
 import org.apache.maven.api.annotations.Experimental;
 
 /**
- *
+ * Factory interface for creating new RequestCache instances.
+ * Implementations should handle the creation and configuration of cache 
instances
+ * based on the current Maven session and environment.
  *
  * @since 4.0.0
+ * @see RequestCache
  */
 @Experimental
-public class ArtifactResolverException extends MavenException {
-
-    @Serial
-    private static final long serialVersionUID = 7252294837746943917L;
+public interface RequestCacheFactory {
 
     /**
-     * @param message the message for the exception
-     * @param e the exception itself
+     * Creates a new RequestCache instance.
+     * The created cache should be configured according to the current Maven 
session
+     * and environment settings.
+     *
+     * @return A new RequestCache instance
      */
-    public ArtifactResolverException(String message, Exception e) {
-        super(message, e);
-    }
+    RequestCache createCache();
 }
diff --git 
a/api/maven-api-core/src/main/java/org/apache/maven/api/cache/RequestResult.java
 
b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/RequestResult.java
new file mode 100644
index 0000000000..2876a3bb6a
--- /dev/null
+++ 
b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/RequestResult.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *   http://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.apache.maven.api.cache;
+
+import org.apache.maven.api.annotations.Experimental;
+import org.apache.maven.api.services.Request;
+import org.apache.maven.api.services.Result;
+
+/**
+ * A record representing the result of a single request operation, containing 
the original request,
+ * the result (if successful), and any error that occurred during processing.
+ * <p>
+ * This class is immutable and thread-safe, suitable for use in concurrent 
operations.
+ *
+ * @param <REQ> The type of the request
+ * @param <REP> The type of the response, which must extend {@code Result<REQ>}
+ * @param request The original request that was processed
+ * @param result The result of the request, if successful; may be null if an 
error occurred
+ * @param error Any error that occurred during processing; null if the request 
was successful
+ * @since 4.0.0
+ */
+@Experimental
+public record RequestResult<REQ extends Request<?>, REP extends Result<REQ>>(
+        /**
+         * The original request that was processed
+         */
+        REQ request,
+
+        /**
+         * The result of the request, if successful; may be null if an error 
occurred
+         */
+        REP result,
+
+        /**
+         * Any error that occurred during processing; null if the request was 
successful
+         */
+        Throwable error) {
+
+    /**
+     * Determines if the request was processed successfully.
+     *
+     * @return true if no error occurred during processing (error is null), 
false otherwise
+     */
+    public boolean isSuccess() {
+        return error == null;
+    }
+}
diff --git 
a/api/maven-api-core/src/main/java/org/apache/maven/api/cache/package-info.java 
b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/package-info.java
new file mode 100644
index 0000000000..9e6a0f4585
--- /dev/null
+++ 
b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/package-info.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *   http://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.
+ */
+
+/**
+ * Provides a caching infrastructure for Maven requests and their results.
+ * <p>
+ * This package contains the core components for implementing and managing 
caches in Maven:
+ * <ul>
+ *   <li>{@link org.apache.maven.api.cache.RequestCache} - The main interface 
for caching request results</li>
+ *   <li>{@link org.apache.maven.api.cache.RequestCacheFactory} - Factory for 
creating cache instances</li>
+ *   <li>{@link org.apache.maven.api.cache.CacheMetadata} - Configuration for 
cache behavior and lifecycle</li>
+ * </ul>
+ * <p>
+ * The caching system supports different retention periods through {@link 
org.apache.maven.api.cache.CacheRetention}:
+ * <ul>
+ *   <li>PERSISTENT - Data persists across Maven invocations</li>
+ *   <li>SESSION_SCOPED - Data retained for the duration of a Maven 
session</li>
+ *   <li>REQUEST_SCOPED - Data retained only for the current build request</li>
+ *   <li>DISABLED - No caching performed</li>
+ * </ul>
+ * <p>
+ * Example usage:
+ * <pre>
+ * RequestCache cache = cacheFactory.createCache();
+ * Result result = cache.request(myRequest, req -> {
+ *     // Expensive operation to compute result
+ *     return computedResult;
+ * });
+ * </pre>
+ * <p>
+ * The package also provides support for batch operations through {@link 
org.apache.maven.api.cache.BatchRequestException}
+ * and {@link org.apache.maven.api.cache.RequestResult} which help manage 
multiple requests and their results.
+ *
+ * @since 4.0.0
+ */
+@Experimental
+package org.apache.maven.api.cache;
+
+import org.apache.maven.api.annotations.Experimental;
diff --git 
a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverException.java
 
b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverException.java
index 05831ce172..22f398c065 100644
--- 
a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverException.java
+++ 
b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverException.java
@@ -33,11 +33,19 @@ public class ArtifactResolverException extends 
MavenException {
     @Serial
     private static final long serialVersionUID = 7252294837746943917L;
 
+    private final ArtifactResolverResult result;
+
     /**
      * @param message the message for the exception
      * @param e the exception itself
+     * @param result the resolution result containing detailed information
      */
-    public ArtifactResolverException(String message, Exception e) {
+    public ArtifactResolverException(String message, Exception e, 
ArtifactResolverResult result) {
         super(message, e);
+        this.result = result;
+    }
+
+    public ArtifactResolverResult getResult() {
+        return result;
     }
 }
diff --git 
a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverRequest.java
 
b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverRequest.java
index 36336eae08..fb012fab30 100644
--- 
a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverRequest.java
+++ 
b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverRequest.java
@@ -45,7 +45,7 @@ public interface ArtifactResolverRequest extends 
Request<Session> {
     @Nonnull
     Collection<? extends ArtifactCoordinates> getCoordinates();
 
-    @Nonnull
+    @Nullable
     List<RemoteRepository> getRepositories();
 
     @Nonnull
@@ -155,6 +155,7 @@ public int hashCode() {
             }
 
             @Override
+            @Nonnull
             public String toString() {
                 return "ArtifactResolverRequest[" + "coordinates="
                         + coordinates + ", repositories="
diff --git 
a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverResult.java
 
b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverResult.java
index f00f7b2337..5f76c2c7bf 100644
--- 
a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverResult.java
+++ 
b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverResult.java
@@ -20,26 +20,118 @@
 
 import java.nio.file.Path;
 import java.util.Collection;
+import java.util.List;
+import java.util.Map;
 
 import org.apache.maven.api.Artifact;
+import org.apache.maven.api.ArtifactCoordinates;
 import org.apache.maven.api.DownloadedArtifact;
+import org.apache.maven.api.Repository;
 import org.apache.maven.api.annotations.Experimental;
 import org.apache.maven.api.annotations.Nonnull;
 import org.apache.maven.api.annotations.Nullable;
 
 /**
- * The Artifact Result
+ * Represents the result of resolving an artifact.
+ * <p>
+ * This interface provides access to resolved artifacts, their associated 
paths, and any related exceptions that
+ * occurred during the resolution process.
+ * </p>
  *
  * @since 4.0.0
  */
 @Experimental
 public interface ArtifactResolverResult extends 
Result<ArtifactResolverRequest> {
+
     /**
-     * @return {@link Artifact}
+     * Returns a collection of resolved artifacts.
+     *
+     * @return A collection of {@link DownloadedArtifact} instances 
representing the resolved artifacts.
      */
     @Nonnull
     Collection<DownloadedArtifact> getArtifacts();
 
+    /**
+     * Retrieves the file system path associated with a specific artifact.
+     *
+     * @param artifact The {@link Artifact} whose path is to be retrieved.
+     * @return The {@link Path} to the artifact, or {@code null} if 
unavailable.
+     */
     @Nullable
-    Path getPath(Artifact artifact);
+    Path getPath(@Nonnull Artifact artifact);
+
+    /**
+     * Returns a mapping of artifact coordinates to their corresponding 
resolution results.
+     *
+     * @return A {@link Map} where keys are {@link ArtifactCoordinates} and 
values are {@link ResultItem} instances.
+     */
+    @Nonnull
+    Map<? extends ArtifactCoordinates, ResultItem> getResults();
+
+    /**
+     * Retrieves the resolution result for a specific set of artifact 
coordinates.
+     *
+     * @param coordinates The {@link ArtifactCoordinates} identifying the 
artifact.
+     * @return The corresponding {@link ResultItem}, or {@code null} if no 
result exists.
+     */
+    default ResultItem getResult(ArtifactCoordinates coordinates) {
+        return getResults().get(coordinates);
+    }
+
+    /**
+     * Represents an individual resolution result for an artifact.
+     */
+    interface ResultItem {
+
+        /**
+         * Returns the coordinates of the resolved artifact.
+         *
+         * @return The {@link ArtifactCoordinates} of the artifact.
+         */
+        ArtifactCoordinates getCoordinates();
+
+        /**
+         * Returns the resolved artifact.
+         *
+         * @return The {@link DownloadedArtifact} instance.
+         */
+        DownloadedArtifact getArtifact();
+
+        /**
+         * Returns a mapping of repositories to the exceptions encountered 
while resolving the artifact.
+         *
+         * @return A {@link Map} where keys are {@link Repository} instances 
and values are {@link Exception} instances.
+         */
+        Map<Repository, List<Exception>> getExceptions();
+
+        /**
+         * Returns the repository from which the artifact was resolved.
+         *
+         * @return The {@link Repository} instance.
+         */
+        Repository getRepository();
+
+        /**
+         * Returns the file system path to the resolved artifact.
+         *
+         * @return The {@link Path} to the artifact.
+         */
+        Path getPath();
+
+        /**
+         * Indicates whether the requested artifact was resolved. Note that 
the artifact might have been successfully
+         * resolved despite {@link #getExceptions()} indicating transfer 
errors while trying to fetch the artifact from some
+         * of the specified remote repositories.
+         *
+         * @return {@code true} if the artifact was resolved, {@code false} 
otherwise.
+         */
+        boolean isResolved();
+
+        /**
+         * Indicates whether the requested artifact is not present in any of 
the specified repositories.
+         *
+         * @return {@code true} if the artifact is not present in any 
repository, {@code false} otherwise.
+         */
+        boolean isMissing();
+    }
 }
diff --git 
a/api/maven-api-core/src/main/java/org/apache/maven/api/services/LocalRepositoryManager.java
 
b/api/maven-api-core/src/main/java/org/apache/maven/api/services/LocalRepositoryManager.java
index 90bcce953b..70d7c9216f 100644
--- 
a/api/maven-api-core/src/main/java/org/apache/maven/api/services/LocalRepositoryManager.java
+++ 
b/api/maven-api-core/src/main/java/org/apache/maven/api/services/LocalRepositoryManager.java
@@ -29,8 +29,26 @@
 import org.apache.maven.api.annotations.Nonnull;
 
 /**
+ * Manages the organization and access of artifacts within the local Maven 
repository.
+ * The local repository serves as a cache for downloaded remote artifacts and 
storage
+ * for locally installed artifacts. This manager provides services to 
determine the
+ * appropriate paths for artifacts within the local repository structure.
+ *
+ * <p>The LocalRepositoryManager is responsible for:
+ * <ul>
+ *   <li>Determining the storage path for locally installed artifacts</li>
+ *   <li>Managing the layout and organization of cached remote artifacts</li>
+ *   <li>Maintaining consistency in artifact storage patterns</li>
+ * </ul>
+ *
+ * <p>This interface is part of Maven's repository management system and works 
in
+ * conjunction with {@link RemoteRepository} and {@link LocalRepository} to 
provide
+ * a complete artifact resolution and storage solution.
  *
  * @since 4.0.0
+ * @see LocalRepository
+ * @see RemoteRepository
+ * @see Artifact
  */
 @Experimental
 public interface LocalRepositoryManager extends Service {
diff --git 
a/api/maven-api-core/src/main/java/org/apache/maven/api/services/Sources.java 
b/api/maven-api-core/src/main/java/org/apache/maven/api/services/Sources.java
index 6911b9156f..4acab6006a 100644
--- 
a/api/maven-api-core/src/main/java/org/apache/maven/api/services/Sources.java
+++ 
b/api/maven-api-core/src/main/java/org/apache/maven/api/services/Sources.java
@@ -28,6 +28,8 @@
 import org.apache.maven.api.annotations.Experimental;
 import org.apache.maven.api.annotations.Nonnull;
 import org.apache.maven.api.annotations.Nullable;
+import org.apache.maven.api.cache.CacheMetadata;
+import org.apache.maven.api.cache.CacheRetention;
 
 import static java.util.Objects.requireNonNull;
 
@@ -191,9 +193,17 @@ public ModelSource resolve(@Nonnull ModelLocator 
modelLocator, @Nonnull String r
 
     /**
      * Implementation of {@link ModelSource} that extends {@link PathSource} 
with model-specific
-     * functionality.
+     * functionality. This implementation uses request-scoped caching ({@link 
CacheRetention#REQUEST_SCOPED})
+     * since it represents a POM file that is actively being built and may 
change during the build process.
+     * <p>
+     * The request-scoped retention policy ensures that:
+     * <ul>
+     *   <li>Changes to the POM file during the build are detected</li>
+     *   <li>Cache entries don't persist beyond the current build request</li>
+     *   <li>Memory is freed once the build request completes</li>
+     * </ul>
      */
-    static class BuildPathSource extends PathSource implements ModelSource {
+    static class BuildPathSource extends PathSource implements ModelSource, 
CacheMetadata {
 
         /**
          * Constructs a new ModelPathSource.
@@ -225,5 +235,10 @@ public ModelSource resolve(@Nonnull ModelLocator locator, 
@Nonnull String relati
             }
             return null;
         }
+
+        @Override
+        public CacheRetention getCacheRetention() {
+            return CacheRetention.REQUEST_SCOPED;
+        }
     }
 }
diff --git 
a/compat/maven-compat/src/test/java/org/apache/maven/repository/LegacyRepositorySystemTest.java
 
b/compat/maven-compat/src/test/java/org/apache/maven/repository/LegacyRepositorySystemTest.java
index ee287309a3..0e0dd44539 100644
--- 
a/compat/maven-compat/src/test/java/org/apache/maven/repository/LegacyRepositorySystemTest.java
+++ 
b/compat/maven-compat/src/test/java/org/apache/maven/repository/LegacyRepositorySystemTest.java
@@ -36,6 +36,7 @@
 import org.apache.maven.execution.MavenSession;
 import org.apache.maven.impl.DefaultRepositoryFactory;
 import org.apache.maven.impl.InternalSession;
+import org.apache.maven.impl.cache.DefaultRequestCacheFactory;
 import org.apache.maven.internal.impl.DefaultSession;
 import org.apache.maven.model.Dependency;
 import org.apache.maven.model.Repository;
@@ -132,8 +133,10 @@ void 
testThatASystemScopedDependencyIsNotResolvedFromRepositories() throws Excep
                 null,
                 null,
                 null,
-                new SimpleLookup(List.of(new DefaultRepositoryFactory(new 
DefaultRemoteRepositoryManager(
-                        new DefaultUpdatePolicyAnalyzer(), new 
DefaultChecksumPolicyProvider())))),
+                new SimpleLookup(List.of(
+                        new DefaultRequestCacheFactory(),
+                        new DefaultRepositoryFactory(new 
DefaultRemoteRepositoryManager(
+                                new DefaultUpdatePolicyAnalyzer(), new 
DefaultChecksumPolicyProvider())))),
                 null);
         InternalSession.associate(session, iSession);
 
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/extensions/BootstrapCoreExtensionManager.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/extensions/BootstrapCoreExtensionManager.java
index e02a675292..d0630453df 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/extensions/BootstrapCoreExtensionManager.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/extensions/BootstrapCoreExtensionManager.java
@@ -31,6 +31,7 @@
 import org.apache.maven.api.Service;
 import org.apache.maven.api.Session;
 import org.apache.maven.api.annotations.Nullable;
+import org.apache.maven.api.cache.RequestCacheFactory;
 import org.apache.maven.api.cli.extensions.CoreExtension;
 import org.apache.maven.api.di.Inject;
 import org.apache.maven.api.di.Named;
@@ -43,6 +44,7 @@
 import org.apache.maven.api.services.RepositoryFactory;
 import org.apache.maven.api.services.VersionParser;
 import org.apache.maven.api.services.VersionRangeResolver;
+import org.apache.maven.cling.invoker.ProtoLookup;
 import org.apache.maven.execution.DefaultMavenExecutionResult;
 import org.apache.maven.execution.MavenExecutionRequest;
 import org.apache.maven.execution.MavenSession;
@@ -55,6 +57,7 @@
 import org.apache.maven.impl.DefaultVersionParser;
 import org.apache.maven.impl.DefaultVersionRangeResolver;
 import org.apache.maven.impl.InternalSession;
+import org.apache.maven.impl.cache.DefaultRequestCacheFactory;
 import org.apache.maven.impl.model.DefaultInterpolator;
 import org.apache.maven.internal.impl.DefaultArtifactManager;
 import org.apache.maven.internal.impl.DefaultSession;
@@ -250,7 +253,15 @@ static class SimpleSession extends DefaultSession {
                 MavenSession session,
                 RepositorySystem repositorySystem,
                 List<org.apache.maven.api.RemoteRepository> repositories) {
-            super(session, repositorySystem, repositories, null, null, null);
+            super(
+                    session,
+                    repositorySystem,
+                    repositories,
+                    null,
+                    ProtoLookup.builder()
+                            .addMapping(RequestCacheFactory.class, new 
DefaultRequestCacheFactory())
+                            .build(),
+                    null);
         }
 
         @Override
diff --git 
a/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java
 
b/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java
index 23d94c7e85..fce6b5dbfa 100644
--- 
a/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java
+++ 
b/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java
@@ -42,6 +42,7 @@
 import org.apache.maven.impl.DefaultModelVersionParser;
 import org.apache.maven.impl.DefaultVersionParser;
 import org.apache.maven.impl.InternalSession;
+import org.apache.maven.impl.cache.DefaultRequestCacheFactory;
 import org.apache.maven.impl.resolver.MavenVersionScheme;
 import org.apache.maven.internal.impl.InternalMavenSession;
 import org.apache.maven.internal.transformation.AbstractRepositoryTestCase;
@@ -77,6 +78,7 @@ protected List<Object> getSessionServices() {
         Mockito.when(node.getChildren()).thenReturn(List.of(child));
 
         services.addAll(List.of(
+                new DefaultRequestCacheFactory(),
                 new DefaultArtifactCoordinatesFactory(),
                 new DefaultDependencyCoordinatesFactory(),
                 new DefaultVersionParser(new DefaultModelVersionParser(new 
MavenVersionScheme())),
diff --git 
a/impl/maven-core/src/test/java/org/apache/maven/model/ModelBuilderTest.java 
b/impl/maven-core/src/test/java/org/apache/maven/model/ModelBuilderTest.java
index f164d79448..7b6ff839f2 100644
--- a/impl/maven-core/src/test/java/org/apache/maven/model/ModelBuilderTest.java
+++ b/impl/maven-core/src/test/java/org/apache/maven/model/ModelBuilderTest.java
@@ -33,6 +33,7 @@
 import org.apache.maven.execution.MavenSession;
 import org.apache.maven.impl.DefaultRepositoryFactory;
 import org.apache.maven.impl.InternalSession;
+import org.apache.maven.impl.cache.DefaultRequestCacheFactory;
 import org.apache.maven.internal.impl.DefaultSession;
 import org.apache.maven.project.DefaultProjectBuildingRequest;
 import org.apache.maven.project.ProjectBuilder;
@@ -80,8 +81,10 @@ void testModelBuilder() throws Exception {
                 repositorySystem,
                 null,
                 mavenRepositorySystem,
-                new SimpleLookup(List.of(new DefaultRepositoryFactory(new 
DefaultRemoteRepositoryManager(
-                        new DefaultUpdatePolicyAnalyzer(), new 
DefaultChecksumPolicyProvider())))),
+                new SimpleLookup(List.of(
+                        new DefaultRequestCacheFactory(),
+                        new DefaultRepositoryFactory(new 
DefaultRemoteRepositoryManager(
+                                new DefaultUpdatePolicyAnalyzer(), new 
DefaultChecksumPolicyProvider())))),
                 null);
         InternalSession.associate(rsession, session);
 
diff --git 
a/impl/maven-impl/src/main/java/org/apache/maven/impl/AbstractSession.java 
b/impl/maven-impl/src/main/java/org/apache/maven/impl/AbstractSession.java
index 0f511ae09e..6025a06475 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/AbstractSession.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/AbstractSession.java
@@ -61,8 +61,11 @@
 import org.apache.maven.api.Version;
 import org.apache.maven.api.VersionConstraint;
 import org.apache.maven.api.VersionRange;
+import org.apache.maven.api.WorkspaceRepository;
 import org.apache.maven.api.annotations.Nonnull;
 import org.apache.maven.api.annotations.Nullable;
+import org.apache.maven.api.cache.RequestCache;
+import org.apache.maven.api.cache.RequestCacheFactory;
 import org.apache.maven.api.model.Repository;
 import org.apache.maven.api.services.ArtifactCoordinatesFactory;
 import org.apache.maven.api.services.ArtifactDeployer;
@@ -97,6 +100,7 @@
 import org.eclipse.aether.RepositorySystem;
 import org.eclipse.aether.RepositorySystemSession;
 import org.eclipse.aether.artifact.ArtifactType;
+import org.eclipse.aether.repository.ArtifactRepository;
 import org.eclipse.aether.transfer.TransferResource;
 
 import static org.apache.maven.impl.Utils.map;
@@ -118,8 +122,7 @@ public abstract class AbstractSession implements 
InternalSession {
             Collections.synchronizedMap(new WeakHashMap<>());
     private final Map<org.eclipse.aether.graph.Dependency, Dependency> 
allDependencies =
             Collections.synchronizedMap(new WeakHashMap<>());
-
-    private final Map<Object, CachingSupplier<?, ?>> requestCache;
+    private volatile RequestCache requestCache;
 
     static {
         TransferResource.setClock(MonotonicClock.get());
@@ -135,82 +138,72 @@ public AbstractSession(
         this.repositorySystem = repositorySystem;
         this.repositories = getRepositories(repositories, 
resolverRepositories);
         this.lookup = lookup;
-        this.requestCache = new ConcurrentHashMap<>();
     }
 
     @Override
     public <REQ extends Request<?>, REP extends Result<REQ>> REP request(REQ 
req, Function<REQ, REP> supplier) {
-        if (requestCache == null) {
-            return supplier.apply(req);
-        }
-        @SuppressWarnings("all")
-        CachingSupplier<REQ, REP> cs =
-                (CachingSupplier) requestCache.computeIfAbsent(req, r -> new 
CachingSupplier<>(supplier));
-        return cs.apply(req);
+        return getRequestCache().request(req, supplier);
     }
 
-    /**
-     * A caching supplier wrapper that caches results and exceptions from the 
underlying supplier.
-     * Used internally to cache expensive computations in the session.
-     *
-     * @param <REQ> The request type
-     * @param <REP> The response type
-     */
-    static class CachingSupplier<REQ, REP> implements Function<REQ, REP> {
-        final Function<REQ, REP> supplier;
-        volatile Object value;
-
-        CachingSupplier(Function<REQ, REP> supplier) {
-            this.supplier = supplier;
-        }
+    @Override
+    public <REQ extends Request<?>, REP extends Result<REQ>> List<REP> 
requests(
+            List<REQ> reqs, Function<List<REQ>, List<REP>> supplier) {
+        return getRequestCache().requests(reqs, supplier);
+    }
 
-        @Override
-        @SuppressWarnings({"unchecked", "checkstyle:InnerAssignment"})
-        public REP apply(REQ req) {
-            Object v;
-            if ((v = value) == null) {
-                synchronized (this) {
-                    if ((v = value) == null) {
-                        try {
-                            v = value = supplier.apply(req);
-                        } catch (Exception e) {
-                            v = value = new CachingSupplier.AltRes(e);
-                        }
-                    }
+    private RequestCache getRequestCache() {
+        RequestCache cache = requestCache;
+        if (cache == null) {
+            synchronized (this) {
+                cache = requestCache;
+                if (cache == null) {
+                    RequestCacheFactory factory = 
lookup.lookup(RequestCacheFactory.class);
+                    requestCache = factory.createCache();
+                    cache = requestCache;
                 }
             }
-            if (v instanceof CachingSupplier.AltRes altRes) {
-                uncheckedThrow(altRes.t);
-            }
-            return (REP) v;
         }
+        return cache;
+    }
 
-        /**
-         * Special holder class for exceptions that occur during supplier 
execution.
-         * Allows caching and re-throwing of exceptions on subsequent calls.
-         */
-        static class AltRes {
-            final Throwable t;
-
-            /**
-             * Creates a new AltRes with the given throwable.
-             *
-             * @param t The throwable to store
-             */
-            AltRes(Throwable t) {
-                this.t = t;
-            }
-        }
+    @Override
+    public RemoteRepository 
getRemoteRepository(org.eclipse.aether.repository.RemoteRepository repository) {
+        return allRepositories.computeIfAbsent(repository, 
DefaultRemoteRepository::new);
     }
 
-    @SuppressWarnings("unchecked")
-    static <T extends Throwable> void uncheckedThrow(Throwable t) throws T {
-        throw (T) t; // rely on vacuous cast
+    @Override
+    public LocalRepository 
getLocalRepository(org.eclipse.aether.repository.LocalRepository repository) {
+        return new DefaultLocalRepository(repository);
     }
 
     @Override
-    public RemoteRepository 
getRemoteRepository(org.eclipse.aether.repository.RemoteRepository repository) {
-        return allRepositories.computeIfAbsent(repository, 
DefaultRemoteRepository::new);
+    public WorkspaceRepository 
getWorkspaceRepository(org.eclipse.aether.repository.WorkspaceRepository 
repository) {
+        return new WorkspaceRepository() {
+            @Nonnull
+            @Override
+            public String getId() {
+                return repository.getId();
+            }
+
+            @Nonnull
+            @Override
+            public String getType() {
+                return repository.getContentType();
+            }
+        };
+    }
+
+    @Override
+    public org.apache.maven.api.Repository getRepository(ArtifactRepository 
repository) {
+        if (repository instanceof 
org.eclipse.aether.repository.RemoteRepository remote) {
+            return getRemoteRepository(remote);
+        } else if (repository instanceof 
org.eclipse.aether.repository.LocalRepository local) {
+            return getLocalRepository(local);
+        } else if (repository instanceof 
org.eclipse.aether.repository.WorkspaceRepository workspace) {
+            return getWorkspaceRepository(workspace);
+        } else {
+            throw new IllegalArgumentException("Unsupported repository type: " 
+ repository.getClass());
+        }
     }
 
     @Override
@@ -624,9 +617,11 @@ public ProducedArtifact createProducedArtifact(
     public DownloadedArtifact resolveArtifact(ArtifactCoordinates coordinates) 
{
         return getService(ArtifactResolver.class)
                 .resolve(this, Collections.singletonList(coordinates))
-                .getArtifacts()
+                .getResults()
+                .values()
                 .iterator()
-                .next();
+                .next()
+                .getArtifact();
     }
 
     /**
@@ -639,9 +634,11 @@ public DownloadedArtifact 
resolveArtifact(ArtifactCoordinates coordinates) {
     public DownloadedArtifact resolveArtifact(ArtifactCoordinates coordinates, 
List<RemoteRepository> repositories) {
         return getService(ArtifactResolver.class)
                 .resolve(this, Collections.singletonList(coordinates), 
repositories)
-                .getArtifacts()
+                .getResults()
+                .values()
                 .iterator()
-                .next();
+                .next()
+                .getArtifact();
     }
 
     /**
diff --git 
a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultArtifactResolver.java
 
b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultArtifactResolver.java
index 8331100d23..244b6b990d 100644
--- 
a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultArtifactResolver.java
+++ 
b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultArtifactResolver.java
@@ -21,24 +21,35 @@
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import org.apache.maven.api.Artifact;
 import org.apache.maven.api.ArtifactCoordinates;
 import org.apache.maven.api.DownloadedArtifact;
+import org.apache.maven.api.Repository;
+import org.apache.maven.api.Session;
+import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.annotations.Nullable;
+import org.apache.maven.api.cache.BatchRequestException;
+import org.apache.maven.api.cache.MavenExecutionException;
 import org.apache.maven.api.di.Named;
 import org.apache.maven.api.di.Singleton;
-import org.apache.maven.api.services.ArtifactManager;
 import org.apache.maven.api.services.ArtifactResolver;
 import org.apache.maven.api.services.ArtifactResolverException;
 import org.apache.maven.api.services.ArtifactResolverRequest;
 import org.apache.maven.api.services.ArtifactResolverResult;
+import org.apache.maven.api.services.Request;
+import org.apache.maven.api.services.RequestTrace;
+import org.apache.maven.api.services.Result;
 import org.eclipse.aether.repository.RemoteRepository;
 import org.eclipse.aether.resolution.ArtifactRequest;
 import org.eclipse.aether.resolution.ArtifactResolutionException;
 import org.eclipse.aether.resolution.ArtifactResult;
+import org.eclipse.aether.transfer.ArtifactNotFoundException;
 
 import static org.apache.maven.impl.Utils.nonNull;
 
@@ -54,70 +65,197 @@ public ArtifactResolverResult 
resolve(ArtifactResolverRequest request)
         return session.request(request, this::doResolve);
     }
 
+    record ResolverRequest(Session session, RequestTrace trace, 
ArtifactRequest request) implements Request<Session> {
+        @Nonnull
+        @Override
+        public Session getSession() {
+            return session;
+        }
+
+        @Nullable
+        @Override
+        public RequestTrace getTrace() {
+            return trace;
+        }
+    }
+
+    record ResolverResult(ResolverRequest request, ArtifactResult result) 
implements Result<ResolverRequest> {
+        @Nonnull
+        @Override
+        public ResolverRequest getRequest() {
+            return request;
+        }
+    }
+
     protected ArtifactResolverResult doResolve(ArtifactResolverRequest 
request) {
         InternalSession session = InternalSession.from(request.getSession());
         RequestTraceHelper.ResolverTrace trace = 
RequestTraceHelper.enter(session, request);
         try {
-            Map<DownloadedArtifact, Path> paths = new HashMap<>();
-            ArtifactManager artifactManager = 
session.getService(ArtifactManager.class);
             List<RemoteRepository> repositories = session.toRepositories(
                     request.getRepositories() != null ? 
request.getRepositories() : session.getRemoteRepositories());
-            List<ArtifactRequest> requests = new ArrayList<>();
+
+            List<ResolverRequest> requests = new ArrayList<>();
             for (ArtifactCoordinates coords : request.getCoordinates()) {
-                org.eclipse.aether.artifact.Artifact aetherArtifact = 
session.toArtifact(coords);
-                Artifact artifact = session.getArtifact(aetherArtifact);
-                Path path = artifactManager.getPath(artifact).orElse(null);
-                if (path != null) {
-                    if (aetherArtifact.getPath() == null) {
-                        aetherArtifact = aetherArtifact.setPath(path);
-                    }
-                    DownloadedArtifact resolved = 
session.getArtifact(DownloadedArtifact.class, aetherArtifact);
-                    paths.put(resolved, path);
-                } else {
-                    requests.add(
-                            new ArtifactRequest(aetherArtifact, repositories, 
trace.context()).setTrace(trace.trace()));
-                }
+                ArtifactRequest req = new ArtifactRequest();
+                req.setRepositories(repositories);
+                req.setArtifact(session.toArtifact(coords));
+                req.setTrace(trace.trace());
+                requests.add(new ResolverRequest(session, trace.mvnTrace(), 
req));
             }
-            if (!requests.isEmpty()) {
-                List<ArtifactResult> results =
-                        
session.getRepositorySystem().resolveArtifacts(session.getSession(), requests);
-                for (ArtifactResult result : results) {
-                    DownloadedArtifact artifact = 
session.getArtifact(DownloadedArtifact.class, result.getArtifact());
-                    Path path = result.getArtifact().getPath();
-                    paths.put(artifact, path);
+            List<ResolverResult> results = session.requests(requests, list -> {
+                try {
+                    List<ArtifactRequest> resolverRequests =
+                            
list.stream().map(ResolverRequest::request).toList();
+                    List<ArtifactResult> resolverResults =
+                            
session.getRepositorySystem().resolveArtifacts(session.getSession(), 
resolverRequests);
+                    List<ResolverResult> res = new 
ArrayList<>(resolverResults.size());
+                    for (int i = 0; i < resolverResults.size(); i++) {
+                        res.add(new ResolverResult(list.get(i), 
resolverResults.get(i)));
+                    }
+                    return res;
+                } catch (ArtifactResolutionException e) {
+                    throw new MavenExecutionException(e);
                 }
+            });
+
+            return toResult(request, results.stream());
+        } catch (BatchRequestException e) {
+            String message;
+            if (e.getResults().size() == 1) {
+                message = 
e.getResults().iterator().next().error().getMessage();
+            } else {
+                message = "Unable to resolve artifacts: " + e.getMessage();
             }
-            return new DefaultArtifactResolverResult(request, paths);
-        } catch (ArtifactResolutionException e) {
-            throw new ArtifactResolverException("Unable to resolve artifact: " 
+ e.getMessage(), e);
+            throw new ArtifactResolverException(message, e, toResult(request, 
e));
         } finally {
             RequestTraceHelper.exit(trace);
         }
     }
 
-    static class DefaultArtifactResolverResult implements 
ArtifactResolverResult {
+    ArtifactResolverResult toResult(ArtifactResolverRequest request, 
BatchRequestException exception) {
+        return toResult(
+                request,
+                exception.getResults().stream()
+                        .map(rr -> {
+                            if (rr.result() != null) {
+                                return rr.result();
+                            } else if (rr.error() != null) {
+                                return new ResolverResult(null, 
((ArtifactResolutionException) rr.error()).getResult());
+                            } else {
+                                throw new IllegalStateException("Unexpected 
result: " + rr);
+                            }
+                        })
+                        .map(ResolverResult.class::cast));
+    }
+
+    ArtifactResolverResult toResult(ArtifactResolverRequest request, 
Stream<ResolverResult> results) {
+        InternalSession session = InternalSession.from(request.getSession());
+        Map<ArtifactCoordinates, ArtifactResolverResult.ResultItem> items = 
results.map(resolverResult -> {
+                    ArtifactResult result = resolverResult.result();
+                    DownloadedArtifact artifact = result.getArtifact() != null
+                            ? session.getArtifact(DownloadedArtifact.class, 
result.getArtifact())
+                            : null;
+                    ArtifactCoordinates coordinates = session.getArtifact(
+                                    result.getRequest().getArtifact())
+                            .toCoordinates();
+                    Repository repository =
+                            result.getRepository() != null ? 
session.getRepository(result.getRepository()) : null;
+                    Map<Repository, List<Exception>> mappedExceptions = 
result.getMappedExceptions().entrySet().stream()
+                            .collect(Collectors.toMap(
+                                    entry -> 
session.getRepository(entry.getKey()), Map.Entry::getValue));
+                    return new DefaultArtifactResolverResultItem(
+                            coordinates,
+                            artifact,
+                            mappedExceptions,
+                            repository,
+                            result.getArtifact() != null ? 
result.getArtifact().getPath() : null);
+                })
+                
.collect(Collectors.toMap(DefaultArtifactResolverResultItem::coordinates, 
Function.identity()));
+
+        return new DefaultArtifactResolverResult(request, items);
+    }
+
+    record DefaultArtifactResolverResultItem(
+            @Nonnull ArtifactCoordinates coordinates,
+            @Nullable DownloadedArtifact artifact,
+            @Nonnull Map<Repository, List<Exception>> exceptions,
+            @Nullable Repository repository,
+            Path path)
+            implements ArtifactResolverResult.ResultItem {
+        @Override
+        public ArtifactCoordinates getCoordinates() {
+            return coordinates;
+        }
+
+        @Override
+        public DownloadedArtifact getArtifact() {
+            return artifact;
+        }
 
-        final ArtifactResolverRequest request;
-        final Map<DownloadedArtifact, Path> paths;
+        @Override
+        public Map<Repository, List<Exception>> getExceptions() {
+            return exceptions;
+        }
 
-        DefaultArtifactResolverResult(ArtifactResolverRequest request, 
Map<DownloadedArtifact, Path> paths) {
+        @Override
+        public Repository getRepository() {
+            return repository;
+        }
+
+        @Override
+        public Path getPath() {
+            return path;
+        }
+
+        @Override
+        public boolean isResolved() {
+            return getPath() != null;
+        }
+
+        @Override
+        public boolean isMissing() {
+            return exceptions.values().stream()
+                            .flatMap(List::stream)
+                            .allMatch(e -> e instanceof 
ArtifactNotFoundException)
+                    && !isResolved();
+        }
+    }
+
+    record DefaultArtifactResolverResult(
+            ArtifactResolverRequest request, Map<ArtifactCoordinates, 
ArtifactResolverResult.ResultItem> results)
+            implements ArtifactResolverResult {
+
+        DefaultArtifactResolverResult(ArtifactResolverRequest request, 
Map<ArtifactCoordinates, ResultItem> results) {
             this.request = request;
-            this.paths = paths;
+            this.results = Map.copyOf(results);
         }
 
         @Override
+        @Nonnull
         public ArtifactResolverRequest getRequest() {
             return request;
         }
 
+        @Nonnull
         @Override
         public Collection<DownloadedArtifact> getArtifacts() {
-            return paths.keySet();
+            return 
results.values().stream().map(ResultItem::getArtifact).collect(Collectors.toList());
+        }
+
+        @Override
+        public Path getPath(@Nonnull Artifact artifact) {
+            ResultItem resultItem = results.get(artifact.toCoordinates());
+            return resultItem != null ? resultItem.getPath() : null;
+        }
+
+        @Override
+        public @Nonnull Map<? extends ArtifactCoordinates, ResultItem> 
getResults() {
+            return results;
         }
 
         @Override
-        public Path getPath(Artifact artifact) {
-            return paths.get(artifact);
+        public ResultItem getResult(ArtifactCoordinates coordinates) {
+            return results.get(coordinates);
         }
     }
 }
diff --git 
a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultDependencyResolver.java
 
b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultDependencyResolver.java
index 984c2c90a9..1badc8a299 100644
--- 
a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultDependencyResolver.java
+++ 
b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultDependencyResolver.java
@@ -29,7 +29,6 @@
 
 import org.apache.maven.api.Artifact;
 import org.apache.maven.api.ArtifactCoordinates;
-import org.apache.maven.api.Dependency;
 import org.apache.maven.api.DependencyCoordinates;
 import org.apache.maven.api.DependencyScope;
 import org.apache.maven.api.Node;
@@ -207,10 +206,13 @@ public DependencyResolverResult 
resolve(DependencyResolverRequest request)
                     ArtifactResolverResult artifactResolverResult =
                             
session.getService(ArtifactResolver.class).resolve(session, coordinates, 
repositories);
                     for (Node node : nodes) {
-                        Dependency d = node.getDependency();
-                        Path path = (d != null) ? 
artifactResolverResult.getPath(d) : null;
+                        Path path = (node.getArtifact() != null)
+                                ? artifactResolverResult
+                                        
.getResult(node.getArtifact().toCoordinates())
+                                        .getPath()
+                                : null;
                         try {
-                            resolverResult.addDependency(node, d, filter, 
path);
+                            resolverResult.addDependency(node, 
node.getDependency(), filter, path);
                         } catch (IOException e) {
                             throw cannotReadModuleInfo(path, e);
                         }
diff --git 
a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultLocalRepositoryManager.java
 
b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultLocalRepositoryManager.java
index 4cf41ed3d8..6098de5d74 100644
--- 
a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultLocalRepositoryManager.java
+++ 
b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultLocalRepositoryManager.java
@@ -24,6 +24,7 @@
 import org.apache.maven.api.LocalRepository;
 import org.apache.maven.api.RemoteRepository;
 import org.apache.maven.api.Session;
+import org.apache.maven.api.annotations.Nonnull;
 import org.apache.maven.api.di.Named;
 import org.apache.maven.api.di.Singleton;
 import org.apache.maven.api.services.LocalRepositoryManager;
@@ -32,20 +33,24 @@
 @Singleton
 public class DefaultLocalRepositoryManager implements LocalRepositoryManager {
 
+    @Nonnull
     @Override
-    public Path getPathForLocalArtifact(Session session, LocalRepository 
local, Artifact artifact) {
+    public Path getPathForLocalArtifact(
+            @Nonnull Session session, @Nonnull LocalRepository local, @Nonnull 
Artifact artifact) {
         InternalSession s = InternalSession.from(session);
-        String path = getManager(s, 
local).getPathForLocalArtifact(s.toArtifact(artifact));
-        return local.getPath().resolve(path);
+        return getManager(s, 
local).getAbsolutePathForLocalArtifact(s.toArtifact(artifact));
     }
 
+    @Nonnull
     @Override
     public Path getPathForRemoteArtifact(
-            Session session, LocalRepository local, RemoteRepository remote, 
Artifact artifact) {
+            @Nonnull Session session,
+            @Nonnull LocalRepository local,
+            @Nonnull RemoteRepository remote,
+            @Nonnull Artifact artifact) {
         InternalSession s = InternalSession.from(session);
-        String path =
-                getManager(s, 
local).getPathForRemoteArtifact(s.toArtifact(artifact), s.toRepository(remote), 
null);
-        return local.getPath().resolve(path);
+        return getManager(s, local)
+                .getAbsolutePathForRemoteArtifact(s.toArtifact(artifact), 
s.toRepository(remote), null);
     }
 
     private org.eclipse.aether.repository.LocalRepositoryManager getManager(
diff --git 
a/impl/maven-impl/src/main/java/org/apache/maven/impl/InternalSession.java 
b/impl/maven-impl/src/main/java/org/apache/maven/impl/InternalSession.java
index 63c6fbcfc1..e6d6d4c53f 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/InternalSession.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/InternalSession.java
@@ -29,7 +29,9 @@
 import org.apache.maven.api.LocalRepository;
 import org.apache.maven.api.Node;
 import org.apache.maven.api.RemoteRepository;
+import org.apache.maven.api.Repository;
 import org.apache.maven.api.Session;
+import org.apache.maven.api.WorkspaceRepository;
 import org.apache.maven.api.annotations.Nonnull;
 import org.apache.maven.api.annotations.Nullable;
 import org.apache.maven.api.services.Request;
@@ -70,8 +72,17 @@ static void 
associate(org.eclipse.aether.RepositorySystemSession rsession, Sessi
      */
     <REQ extends Request<?>, REP extends Result<REQ>> REP request(REQ req, 
Function<REQ, REP> supplier);
 
+    <REQ extends Request<?>, REP extends Result<REQ>> List<REP> requests(
+            List<REQ> req, Function<List<REQ>, List<REP>> supplier);
+
     RemoteRepository 
getRemoteRepository(org.eclipse.aether.repository.RemoteRepository repository);
 
+    LocalRepository 
getLocalRepository(org.eclipse.aether.repository.LocalRepository repository);
+
+    WorkspaceRepository 
getWorkspaceRepository(org.eclipse.aether.repository.WorkspaceRepository 
repository);
+
+    Repository getRepository(org.eclipse.aether.repository.ArtifactRepository 
repository);
+
     Node getNode(org.eclipse.aether.graph.DependencyNode node);
 
     Node getNode(org.eclipse.aether.graph.DependencyNode node, boolean 
verbose);
diff --git 
a/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CachingSupplier.java
 
b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CachingSupplier.java
new file mode 100644
index 0000000000..0e70715aea
--- /dev/null
+++ 
b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CachingSupplier.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *   http://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.apache.maven.impl.cache;
+
+import java.util.function.Function;
+
+/**
+ * A caching supplier wrapper that caches results and exceptions from the 
underlying supplier.
+ * Used internally to cache expensive computations in the session.
+ *
+ * @param <REQ> The request type
+ * @param <REP> The response type
+ */
+class CachingSupplier<REQ, REP> implements Function<REQ, REP> {
+    final Function<REQ, REP> supplier;
+    volatile Object value;
+
+    CachingSupplier(Function<REQ, REP> supplier) {
+        this.supplier = supplier;
+    }
+
+    Object getValue() {
+        return value;
+    }
+
+    @Override
+    @SuppressWarnings({"unchecked", "checkstyle:InnerAssignment"})
+    public REP apply(REQ req) {
+        Object v;
+        if ((v = value) == null) {
+            synchronized (this) {
+                if ((v = value) == null) {
+                    try {
+                        v = value = supplier.apply(req);
+                    } catch (Exception e) {
+                        v = value = new AltRes(e);
+                    }
+                }
+            }
+        }
+        if (v instanceof AltRes altRes) {
+            DefaultRequestCache.uncheckedThrow(altRes.t);
+        }
+        return (REP) v;
+    }
+
+    /**
+     * Special holder class for exceptions that occur during supplier 
execution.
+     * Allows caching and re-throwing of exceptions on subsequent calls.
+     */
+    static class AltRes {
+        final Throwable t;
+
+        /**
+         * Creates a new AltRes with the given throwable.
+         *
+         * @param t The throwable to store
+         */
+        AltRes(Throwable t) {
+            this.t = t;
+        }
+    }
+}
diff --git 
a/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/DefaultRequestCache.java
 
b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/DefaultRequestCache.java
new file mode 100644
index 0000000000..6052a605bf
--- /dev/null
+++ 
b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/DefaultRequestCache.java
@@ -0,0 +1,167 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *   http://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.apache.maven.impl.cache;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.function.Function;
+
+import org.apache.maven.api.Session;
+import org.apache.maven.api.SessionData;
+import org.apache.maven.api.cache.BatchRequestException;
+import org.apache.maven.api.cache.CacheMetadata;
+import org.apache.maven.api.cache.CacheRetention;
+import org.apache.maven.api.cache.MavenExecutionException;
+import org.apache.maven.api.cache.RequestCache;
+import org.apache.maven.api.cache.RequestResult;
+import org.apache.maven.api.services.Request;
+import org.apache.maven.api.services.RequestTrace;
+import org.apache.maven.api.services.Result;
+
+public class DefaultRequestCache implements RequestCache {
+
+    private static final SessionData.Key<ConcurrentMap> KEY = 
SessionData.key(ConcurrentMap.class, CacheMetadata.class);
+    private static final Object ROOT = new Object();
+
+    private final Map<Object, CachingSupplier<?, ?>> forever = new 
ConcurrentHashMap<>();
+
+    @Override
+    @SuppressWarnings("all")
+    public <REQ extends Request<?>, REP extends Result<REQ>> REP request(REQ 
req, Function<REQ, REP> supplier) {
+        CachingSupplier<REQ, REP> cs = doCache(req, supplier);
+        return cs.apply(req);
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <REQ extends Request<?>, REP extends Result<REQ>> List<REP> 
requests(
+            List<REQ> reqs, Function<List<REQ>, List<REP>> supplier) {
+        final Map<REQ, Object> nonCachedResults = new HashMap<>();
+        List<RequestResult<REQ, REP>> allResults = new 
ArrayList<>(reqs.size());
+
+        Function<REQ, REP> individualSupplier = req -> {
+            synchronized (nonCachedResults) {
+                while (!nonCachedResults.containsKey(req)) {
+                    try {
+                        nonCachedResults.wait();
+                    } catch (InterruptedException e) {
+                        Thread.currentThread().interrupt();
+                        throw new RuntimeException(e);
+                    }
+                }
+                Object val = nonCachedResults.get(req);
+                if (val instanceof CachingSupplier.AltRes altRes) {
+                    uncheckedThrow(altRes.t);
+                }
+                return (REP) val;
+            }
+        };
+
+        List<CachingSupplier<REQ, REP>> suppliers = new 
ArrayList<>(reqs.size());
+        List<REQ> nonCached = new ArrayList<>();
+        for (REQ req : reqs) {
+            CachingSupplier<REQ, REP> cs = doCache(req, individualSupplier);
+            suppliers.add(cs);
+            if (cs.getValue() == null) {
+                nonCached.add(req);
+            }
+        }
+
+        if (!nonCached.isEmpty()) {
+            synchronized (nonCachedResults) {
+                try {
+                    List<REP> reps = supplier.apply(nonCached);
+                    for (int i = 0; i < reps.size(); i++) {
+                        nonCachedResults.put(nonCached.get(i), reps.get(i));
+                    }
+                } catch (MavenExecutionException e) {
+                    // If batch request fails, mark all non-cached requests as 
failed
+                    for (REQ req : nonCached) {
+                        nonCachedResults.put(
+                                req, new 
CachingSupplier.AltRes(e.getCause())); // Mark as processed but failed
+                    }
+                } finally {
+                    nonCachedResults.notifyAll();
+                }
+            }
+        }
+
+        // Collect results in original order
+        boolean hasFailures = false;
+        for (int i = 0; i < reqs.size(); i++) {
+            REQ req = reqs.get(i);
+            CachingSupplier<REQ, REP> cs = suppliers.get(i);
+            try {
+                REP value = cs.apply(req);
+                allResults.add(new RequestResult<>(req, value, null));
+            } catch (Throwable t) {
+                hasFailures = true;
+                allResults.add(new RequestResult<>(req, null, t));
+            }
+        }
+
+        if (hasFailures) {
+            throw new BatchRequestException("One or more requests failed", 
allResults);
+        }
+
+        return allResults.stream().map(RequestResult::result).toList();
+    }
+
+    @SuppressWarnings("unchecked")
+    private <REQ extends Request<?>, REP extends Result<REQ>> 
CachingSupplier<REQ, REP> doCache(
+            REQ req, Function<REQ, REP> supplier) {
+        CacheRetention retention = Objects.requireNonNullElse(
+                req instanceof CacheMetadata metadata ? 
metadata.getCacheRetention() : null,
+                CacheRetention.SESSION_SCOPED);
+
+        Map<Object, CachingSupplier<?, ?>> cache = null;
+        if ((retention == CacheRetention.REQUEST_SCOPED || retention == 
CacheRetention.SESSION_SCOPED)
+                && req.getSession() instanceof Session session) {
+            Object key = retention == CacheRetention.REQUEST_SCOPED ? 
doGetOuterRequest(req) : ROOT;
+            Map<Object, Map<Object, CachingSupplier<?, ?>>> caches =
+                    session.getData().computeIfAbsent(KEY, 
ConcurrentHashMap::new);
+            cache = caches.computeIfAbsent(key, k -> new WeakIdentityMap<>());
+        } else if (retention == CacheRetention.PERSISTENT) {
+            cache = forever;
+        }
+        if (cache != null) {
+            return (CachingSupplier<REQ, REP>) cache.computeIfAbsent(req, r -> 
new CachingSupplier<>(supplier));
+        } else {
+            return new CachingSupplier<>(supplier);
+        }
+    }
+
+    private <REQ extends Request<?>> Object doGetOuterRequest(REQ req) {
+        RequestTrace trace = req.getTrace();
+        while (trace != null && trace.parent() != null) {
+            trace = trace.parent();
+        }
+        return trace != null && trace.data() != null ? trace.data() : req;
+    }
+
+    @SuppressWarnings("unchecked")
+    static <T extends Throwable> void uncheckedThrow(Throwable t) throws T {
+        throw (T) t; // rely on vacuous cast
+    }
+}
diff --git 
a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverException.java
 
b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/DefaultRequestCacheFactory.java
similarity index 61%
copy from 
api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverException.java
copy to 
impl/maven-impl/src/main/java/org/apache/maven/impl/cache/DefaultRequestCacheFactory.java
index 05831ce172..1600b80388 100644
--- 
a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverException.java
+++ 
b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/DefaultRequestCacheFactory.java
@@ -16,28 +16,18 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.maven.api.services;
+package org.apache.maven.impl.cache;
 
-import java.io.Serial;
+import org.apache.maven.api.cache.RequestCache;
+import org.apache.maven.api.cache.RequestCacheFactory;
+import org.apache.maven.api.di.Named;
+import org.apache.maven.api.di.Singleton;
 
-import org.apache.maven.api.annotations.Experimental;
+@Named
+@Singleton
+public class DefaultRequestCacheFactory implements RequestCacheFactory {
 
-/**
- *
- *
- * @since 4.0.0
- */
-@Experimental
-public class ArtifactResolverException extends MavenException {
-
-    @Serial
-    private static final long serialVersionUID = 7252294837746943917L;
-
-    /**
-     * @param message the message for the exception
-     * @param e the exception itself
-     */
-    public ArtifactResolverException(String message, Exception e) {
-        super(message, e);
+    public RequestCache createCache() {
+        return new DefaultRequestCache();
     }
 }
diff --git 
a/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/WeakIdentityMap.java
 
b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/WeakIdentityMap.java
new file mode 100644
index 0000000000..cf4e7dd7b4
--- /dev/null
+++ 
b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/WeakIdentityMap.java
@@ -0,0 +1,239 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *   http://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.apache.maven.impl.cache;
+
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
+/**
+ * A Map implementation that uses weak references for both keys and values,
+ * and compares keys using identity (==) rather than equals().
+ *
+ * @param <K> the type of keys maintained by this map
+ * @param <V> the type of mapped values
+ */
+public class WeakIdentityMap<K, V> implements Map<K, V> {
+
+    private final ReferenceQueue<K> keyQueue = new ReferenceQueue<>();
+    private final ReferenceQueue<V> valueQueue = new ReferenceQueue<>();
+    private final ConcurrentHashMap<WeakIdentityReference<K>, 
ComputeReference<V>> map = new ConcurrentHashMap<>();
+
+    private static class WeakIdentityReference<T> extends WeakReference<T> {
+        private final int hash;
+
+        WeakIdentityReference(T referent, ReferenceQueue<T> queue) {
+            super(referent, queue);
+            this.hash = referent.hashCode();
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (!(obj instanceof WeakIdentityReference<?> other)) {
+                return false;
+            }
+            T thisRef = this.get();
+            Object otherRef = other.get();
+            return thisRef != null && thisRef.equals(otherRef);
+        }
+
+        @Override
+        public int hashCode() {
+            return hash;
+        }
+    }
+
+    private static class ComputeReference<V> extends WeakReference<V> {
+        private final boolean computing;
+
+        ComputeReference(V value, ReferenceQueue<V> queue) {
+            super(value, queue);
+            this.computing = false;
+        }
+
+        private ComputeReference(ReferenceQueue<V> queue) {
+            super(null, queue);
+            this.computing = true;
+        }
+
+        static <V> ComputeReference<V> computing(ReferenceQueue<V> queue) {
+            return new ComputeReference<>(queue);
+        }
+    }
+
+    @Override
+    public V computeIfAbsent(K key, Function<? super K, ? extends V> 
mappingFunction) {
+        Objects.requireNonNull(key);
+        Objects.requireNonNull(mappingFunction);
+
+        while (true) {
+            expungeStaleEntries();
+
+            WeakIdentityReference<K> weakKey = new 
WeakIdentityReference<>(key, keyQueue);
+
+            // Try to get existing value
+            ComputeReference<V> valueRef = map.get(weakKey);
+            if (valueRef != null && !valueRef.computing) {
+                V value = valueRef.get();
+                if (value != null) {
+                    return value;
+                }
+                // Value was GC'd, remove it
+                map.remove(weakKey, valueRef);
+            }
+
+            // Try to claim computation
+            ComputeReference<V> computingRef = 
ComputeReference.computing(valueQueue);
+            valueRef = map.putIfAbsent(weakKey, computingRef);
+
+            if (valueRef == null) {
+                // We claimed the computation
+                try {
+                    V newValue = mappingFunction.apply(key);
+                    if (newValue == null) {
+                        map.remove(weakKey, computingRef);
+                        return null;
+                    }
+
+                    ComputeReference<V> newValueRef = new 
ComputeReference<>(newValue, valueQueue);
+                    map.replace(weakKey, computingRef, newValueRef);
+                    return newValue;
+                } catch (Throwable t) {
+                    map.remove(weakKey, computingRef);
+                    throw t;
+                }
+            } else if (!valueRef.computing) {
+                // Another thread has a value
+                V value = valueRef.get();
+                if (value != null) {
+                    return value;
+                }
+                // Value was GC'd
+                if (map.remove(weakKey, valueRef)) {
+                    continue;
+                }
+            }
+            // Another thread is computing or the reference changed, try again
+        }
+    }
+
+    private void expungeStaleEntries() {
+        Reference<?> ref;
+        while ((ref = keyQueue.poll()) != null) {
+            map.remove(ref);
+        }
+        while ((ref = valueQueue.poll()) != null) {
+            map.values().remove(ref);
+        }
+    }
+
+    @Override
+    public int size() {
+        expungeStaleEntries();
+        return map.size();
+    }
+
+    @Override
+    public boolean isEmpty() {
+        expungeStaleEntries();
+        return map.isEmpty();
+    }
+
+    @Override
+    public boolean containsKey(Object key) {
+        expungeStaleEntries();
+        return map.containsKey(new WeakIdentityReference<>((K) key, null));
+    }
+
+    @Override
+    public boolean containsValue(Object value) {
+        expungeStaleEntries();
+        for (WeakReference<V> ref : map.values()) {
+            V v = ref.get();
+            if (v != null && v == value) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public V get(Object key) {
+        expungeStaleEntries();
+        WeakReference<V> ref = map.get(new WeakIdentityReference<>((K) key, 
null));
+        return ref != null ? ref.get() : null;
+    }
+
+    @Override
+    public V put(K key, V value) {
+        Objects.requireNonNull(key);
+        Objects.requireNonNull(value);
+        expungeStaleEntries();
+
+        WeakReference<V> oldValueRef =
+                map.put(new WeakIdentityReference<>(key, keyQueue), new 
ComputeReference<>(value, valueQueue));
+
+        return oldValueRef != null ? oldValueRef.get() : null;
+    }
+
+    @Override
+    public V remove(Object key) {
+        expungeStaleEntries();
+        WeakReference<V> valueRef = map.remove(new WeakIdentityReference<>((K) 
key, null));
+        return valueRef != null ? valueRef.get() : null;
+    }
+
+    @Override
+    public void putAll(Map<? extends K, ? extends V> m) {
+        Objects.requireNonNull(m);
+        for (Entry<? extends K, ? extends V> e : m.entrySet()) {
+            put(e.getKey(), e.getValue());
+        }
+    }
+
+    @Override
+    public void clear() {
+        map.clear();
+        expungeStaleEntries();
+    }
+
+    @Override
+    public Set<K> keySet() {
+        throw new UnsupportedOperationException("keySet not supported");
+    }
+
+    @Override
+    public Collection<V> values() {
+        throw new UnsupportedOperationException("values not supported");
+    }
+
+    @Override
+    public Set<Entry<K, V>> entrySet() {
+        throw new UnsupportedOperationException("entrySet not supported");
+    }
+}
diff --git 
a/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/WeakIdentityMapTest.java
 
b/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/WeakIdentityMapTest.java
new file mode 100644
index 0000000000..865e18a68e
--- /dev/null
+++ 
b/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/WeakIdentityMapTest.java
@@ -0,0 +1,196 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *   http://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.apache.maven.impl.cache;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.RepeatedTest;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class WeakIdentityMapTest {
+    private WeakIdentityMap<Object, String> map;
+
+    @BeforeEach
+    void setUp() {
+        map = new WeakIdentityMap<>();
+    }
+
+    @Test
+    void shouldComputeValueOnlyOnce() {
+        Object key = new Object();
+        AtomicInteger computeCount = new AtomicInteger(0);
+
+        String result1 = map.computeIfAbsent(key, k -> {
+            computeCount.incrementAndGet();
+            return "value";
+        });
+
+        String result2 = map.computeIfAbsent(key, k -> {
+            computeCount.incrementAndGet();
+            return "different value";
+        });
+
+        assertEquals("value", result1);
+        assertEquals("value", result2);
+        assertEquals(1, computeCount.get());
+    }
+
+    @RepeatedTest(10)
+    void shouldBeThreadSafe() throws InterruptedException {
+        Consumer<String> sink = s -> {}; // System.out::println;
+
+        int threadCount = 5;
+        int iterationsPerThread = 100;
+        Object key = new Object();
+        AtomicInteger computeCount = new AtomicInteger(0);
+        CountDownLatch startLatch = new CountDownLatch(1);
+        CountDownLatch finishLatch = new CountDownLatch(threadCount);
+        List<String> uniqueResults = new ArrayList<>();
+        CyclicBarrier iterationBarrier = new CyclicBarrier(threadCount);
+
+        // Create and start threads
+        for (int i = 0; i < threadCount; i++) {
+            final int threadId = i;
+            new Thread(
+                            () -> {
+                                try {
+                                    startLatch.await(); // Wait for all 
threads to be ready
+
+                                    // Use AtomicInteger for thread-safe 
iteration counting
+                                    AtomicInteger iteration = new 
AtomicInteger(0);
+                                    while (iteration.get() < 
iterationsPerThread) {
+                                        // Synchronize threads at the start of 
each iteration
+                                        iterationBarrier.await();
+
+                                        String result = 
map.computeIfAbsent(key, k -> {
+                                            sink.accept("Computing value in 
thread " + threadId + " iteration "
+                                                    + iteration.get() + " 
current compute count: "
+                                                    + computeCount.get());
+                                            int count = 
computeCount.incrementAndGet();
+                                            if (count > 1) {
+                                                sink.accept("WARNING: Multiple 
computations detected! Count: " + count);
+                                            }
+                                            return "computed value";
+                                        });
+
+                                        synchronized (uniqueResults) {
+                                            if 
(!uniqueResults.contains(result)) {
+                                                uniqueResults.add(result);
+                                                sink.accept("Added new unique 
result: " + result + " from thread "
+                                                        + threadId);
+                                            }
+                                        }
+
+                                        iteration.incrementAndGet();
+                                    }
+                                } catch (Exception e) {
+                                    e.printStackTrace();
+                                } finally {
+                                    finishLatch.countDown();
+                                    sink.accept("Thread " + threadId + " 
finished");
+                                }
+                            },
+                            "Thread-" + i)
+                    .start();
+        }
+
+        sink.accept("Starting all threads");
+        startLatch.countDown(); // Start all threads
+        finishLatch.await(); // Wait for all threads to finish
+        sink.accept("All threads finished");
+        sink.accept("Final compute count: " + computeCount.get());
+        sink.accept("Unique results size: " + uniqueResults.size());
+
+        assertEquals(
+                1,
+                computeCount.get(),
+                "Value should be computed exactly once, but was computed " + 
computeCount.get() + " times");
+        assertEquals(
+                1,
+                uniqueResults.size(),
+                "All threads should see the same value, but saw " + 
uniqueResults.size() + " different values");
+    }
+
+    @Test
+    void shouldUseIdentityComparison() {
+        // Create two equal but distinct keys
+        String key1 = new String("key");
+        String key2 = new String("key");
+
+        assertTrue(key1.equals(key2), "Sanity check: keys should be equal");
+        assertNotSame(key1, key2, "Sanity check: keys should be distinct 
objects");
+
+        AtomicInteger computeCount = new AtomicInteger(0);
+
+        map.computeIfAbsent(key1, k -> {
+            computeCount.incrementAndGet();
+            return "value1";
+        });
+
+        map.computeIfAbsent(key2, k -> {
+            computeCount.incrementAndGet();
+            return "value2";
+        });
+
+        assertEquals(1, computeCount.get(), "Should compute once for equal but 
distinct keys");
+    }
+
+    @Test
+    void shouldHandleWeakReferences() throws InterruptedException {
+        AtomicInteger computeCount = new AtomicInteger(0);
+
+        // Use a block to ensure the key can be garbage collected
+        {
+            Object key = new Object();
+            map.computeIfAbsent(key, k -> {
+                computeCount.incrementAndGet();
+                return "value";
+            });
+        }
+
+        // Try to force garbage collection
+        System.gc();
+        Thread.sleep(100);
+
+        // Create a new key and verify that computation happens again
+        Object newKey = new Object();
+        map.computeIfAbsent(newKey, k -> {
+            computeCount.incrementAndGet();
+            return "new value";
+        });
+
+        assertEquals(2, computeCount.get(), "Should compute again after 
original key is garbage collected");
+    }
+
+    @Test
+    void shouldHandleNullInputs() {
+        assertThrows(NullPointerException.class, () -> 
map.computeIfAbsent(null, k -> "value"));
+
+        Object key = new Object();
+        assertThrows(NullPointerException.class, () -> 
map.computeIfAbsent(key, null));
+    }
+}


Reply via email to